From 09c401a157958d902bdef9e9fdd2aa168a96533f Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Mon, 26 Dec 2022 11:08:28 +0300 Subject: [PATCH] Voice Chat Fixes & Audio Player Implementation --- lib/provider/chat_provider_model.dart | 47 +----- lib/ui/chat/chat_bubble.dart | 221 +++++++++++++------------- lib/ui/chat/chat_detailed_screen.dart | 4 +- lib/ui/chat/chat_home.dart | 2 + lib/ui/chat/common.dart | 189 ++++++++++++++++++++++ pubspec.yaml | 4 +- 6 files changed, 306 insertions(+), 161 deletions(-) create mode 100644 lib/ui/chat/common.dart diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 6383082..7cb7b96 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -552,8 +552,8 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { msg = voiceFile!.path.split("/").last; } else { msg = message.text; + logger.w(msg); } - logger.w(jsonEncode(repliedMsg)); SingleUserChatModel data = SingleUserChatModel( userChatHistoryId: 0, chatEventId: chatEventId, @@ -1257,51 +1257,6 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { return numberStr; } - // void playRecoding() async { - // isPlaying = true; - // await playerController.startPlayer(finishMode: FinishMode.pause); - //} - - void playVoice( - BuildContext context, { - required SingleUserChatModel data, - }) async { - if (data.voice != null && data.voice!.existsSync()) { - print("Heree"); - await data.voiceController!.setFilePath(data!.voice!.path); - await data.voiceController!.setLoopMode(LoopMode.off); - Duration? duration = await data.voiceController!.load(); - await data.voiceController!.seek(duration); - await data.voiceController!.play(); - } else { - Utils.showLoading(context); - Uint8List encodedString = await ChatApiClient().downloadURL(fileName: data.contant!, fileTypeDescription: getFileTypeDescription(data.fileTypeResponse!.fileTypeName ?? "")); - try { - String path = await downChatVoice(encodedString, data.fileTypeResponse!.fileTypeName ?? "", data); - File file = File(path!); - await file.readAsBytes(); - data.voice = file; - Duration? duration = await data.voiceController!.setFilePath(file.path); - await data.voiceController!.setLoopMode(LoopMode.off); - await data.voiceController!.seek(duration); - await data.voiceController!.setVolume(1.0); - await data.voiceController!.load(); - Utils.hideLoading(context); - await data.voiceController!.play(); - } catch (e) { - Utils.showToast("Cannot open file."); - } - } - } - - void pausePlaying(BuildContext context, {required SingleUserChatModel data}) async { - await data.voiceController!.pause(); - } - - void resumePlaying(BuildContext context, {required SingleUserChatModel data}) async { - await data.voiceController!.play(); - } - Future downChatVoice(Uint8List bytes, String ext, SingleUserChatModel data) async { String dirPath = '${(await getApplicationDocumentsDirectory()).path}/chat_audios'; if (!await Directory(dirPath).exists()) { diff --git a/lib/ui/chat/chat_bubble.dart b/lib/ui/chat/chat_bubble.dart index b54dc3f..824ab48 100644 --- a/lib/ui/chat/chat_bubble.dart +++ b/lib/ui/chat/chat_bubble.dart @@ -1,34 +1,48 @@ import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; +import 'package:flutter/services.dart'; import 'package:audio_waveforms/audio_waveforms.dart' as awf; -import 'package:just_audio/just_audio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:mohem_flutter_app/api/chat/chat_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/models/chat/get_single_user_chat_list_model.dart'; import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; import 'package:mohem_flutter_app/ui/chat/chat_full_image_preview.dart'; +import 'package:mohem_flutter_app/ui/chat/common.dart'; import 'package:provider/provider.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:just_audio/just_audio.dart'; class ChatBubble extends StatelessWidget { ChatBubble({Key? key, required this.dateTime, required this.cItem}) : super(key: key); final String dateTime; final SingleUserChatModel cItem; + bool isCurrentUser = false; + bool isSeen = false; + bool isReplied = false; + int? fileTypeID; + String? fileTypeName; + late ChatProviderModel provider; String? fileTypeDescription; + bool isDelivered = false; + String userName = ''; + late Offset screenOffset; void makeAssign() { @@ -42,6 +56,51 @@ class ChatBubble extends StatelessWidget { userName = AppState().chatDetails!.response!.userName == cItem.currentUserName.toString() ? "You" : cItem.currentUserName.toString(); } + void playVoice( + BuildContext context, { + required SingleUserChatModel data, + }) async { + if (data.voice != null && data.voice!.existsSync()) { + await data.voiceController!.setFilePath(data!.voice!.path); + await data.voiceController!.setLoopMode(LoopMode.off); + Duration? duration = await data.voiceController!.load(); + await data.voiceController!.seek(duration); + await data.voiceController!.play(); + } else { + Utils.showLoading(context); + Uint8List encodedString = await ChatApiClient().downloadURL(fileName: data.contant!, fileTypeDescription: provider.getFileTypeDescription(data.fileTypeResponse!.fileTypeName ?? "")); + try { + String path = await provider.downChatVoice(encodedString, data.fileTypeResponse!.fileTypeName ?? "", data); + File file = File(path!); + await file.readAsBytes(); + data.voice = file; + Duration? duration = await data.voiceController!.setFilePath(file.path); + await data.voiceController!.setLoopMode(LoopMode.off); + await data.voiceController!.seek(duration); + await data.voiceController!.setVolume(1.0); + await data.voiceController!.load(); + Utils.hideLoading(context); + await data.voiceController!.play(); + } catch (e) { + Utils.showToast("Cannot open file."); + } + } + } + + void pausePlaying(BuildContext context, {required SingleUserChatModel data}) async { + await data.voiceController!.pause(); + } + + void rePlay(BuildContext context, {required SingleUserChatModel data}) async { + if (data.voice != null && data.voice!.existsSync()) { + await data.voiceController!.seek(Duration.zero); + await data.voiceController!.play(); + } + } + + Stream get _positionDataStream => Rx.combineLatest3(cItem.voiceController!.positionStream, cItem.voiceController!.bufferedPositionStream, + cItem.voiceController!.durationStream, (Duration position, Duration bufferedPosition, Duration? duration) => PositionData(position, bufferedPosition, duration ?? Duration.zero)); + @override Widget build(BuildContext context) { Size windowSize = MediaQuery.of(context).size; @@ -77,21 +136,18 @@ class ChatBubble extends StatelessWidget { .paddingOnly(right: 5, top: 5, bottom: 8, left: 5), ], ).expanded, - if (cItem.userChatReplyResponse != null && cItem.userChatReplyResponse!.fileTypeId == 12 || - cItem.userChatReplyResponse!.fileTypeId == 3 || - cItem.userChatReplyResponse!.fileTypeId == 4) - ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: SizedBox( - height: 32, - width: 32, - child: showImage( - isReplyPreview: true, + if (cItem.userChatReplyResponse != null) + if (cItem.userChatReplyResponse!.fileTypeId == 12 || cItem.userChatReplyResponse!.fileTypeId == 3 || cItem.userChatReplyResponse!.fileTypeId == 4) + ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: SizedBox( + height: 32, + width: 32, + child: showImage( + isReplyPreview: false, fileName: cItem.userChatReplyResponse!.contant!, - fileTypeDescription: cItem.userChatReplyResponse!.fileTypeResponse!.fileTypeDescription ?? "image/jpg") - .paddingOnly(left: 10, right: 10, bottom: 16, top: 16), - ), - ), + fileTypeDescription: cItem.userChatReplyResponse!.fileTypeResponse!.fileTypeDescription ?? "image/jpg")), + ).paddingOnly(left: 10, right: 10, bottom: 16, top: 16), ], ), ), @@ -113,7 +169,7 @@ class ChatBubble extends StatelessWidget { }), ), ).paddingOnly(bottom: 4), - if (fileTypeID == 13) + if (fileTypeID == 13 && cItem.voiceController != null) currentWaveBubble(context, cItem) else Row( @@ -181,20 +237,19 @@ class ChatBubble extends StatelessWidget { .paddingOnly(right: 5, top: 5, bottom: 8, left: 5), ], ).expanded, - if (cItem.userChatReplyResponse != null && cItem.userChatReplyResponse!.fileTypeId == 12 || - cItem.userChatReplyResponse!.fileTypeId == 3 || - cItem.userChatReplyResponse!.fileTypeId == 4) - ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: SizedBox( - height: 32, - width: 32, - child: showImage( - isReplyPreview: true, - fileName: cItem.userChatReplyResponse!.contant!, - fileTypeDescription: cItem.userChatReplyResponse!.fileTypeResponse!.fileTypeDescription ?? "image/jpg"), - ), - ).paddingOnly(left: 10, right: 10, bottom: 16, top: 16) + if (cItem.userChatReplyResponse != null) + if (cItem.userChatReplyResponse!.fileTypeId == 12 || cItem.userChatReplyResponse!.fileTypeId == 3 || cItem.userChatReplyResponse!.fileTypeId == 4) + ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: SizedBox( + height: 32, + width: 32, + child: showImage( + isReplyPreview: true, + fileName: cItem.userChatReplyResponse!.contant!, + fileTypeDescription: cItem.userChatReplyResponse!.fileTypeResponse!.fileTypeDescription ?? "image/jpg"), + ), + ).paddingOnly(left: 10, right: 10, bottom: 16, top: 16) ], ), ), @@ -216,7 +271,7 @@ class ChatBubble extends StatelessWidget { }), ), ).paddingOnly(bottom: 4), - if (fileTypeID == 13) + if (fileTypeID == 13 && cItem.voiceController != null) recipetWaveBubble(context, cItem) else Row( @@ -293,15 +348,18 @@ class ChatBubble extends StatelessWidget { child: Row( children: [ getPlayer(player: data.voiceController!, modelData: data), - Slider( - activeColor: Colors.white, - inactiveColor: Colors.grey, - value: 0.toDouble(), - max: 50.toDouble(), - onChanged: (double value) { - // Add code to track the music duration. + StreamBuilder( + stream: _positionDataStream, + builder: (BuildContext context, AsyncSnapshot snapshot) { + PositionData? positionData = snapshot.data; + return SeekBar( + duration: positionData?.duration ?? Duration.zero, + position: positionData?.position ?? Duration.zero, + bufferedPosition: positionData?.bufferedPosition ?? Duration.zero, + onChangeEnd: data.voiceController!.seek, + ).expanded; }, - ).expanded, + ), ], ), ).circle(5); @@ -320,15 +378,18 @@ class ChatBubble extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ getPlayer(player: data.voiceController!, modelData: data), - Slider( - activeColor: Colors.white, - inactiveColor: Colors.grey, - value: 0.toDouble(), - max: 50.toDouble(), - onChanged: (double value) { - // Add code to track the music duration. + StreamBuilder( + stream: _positionDataStream, + builder: (BuildContext context, AsyncSnapshot snapshot) { + PositionData? positionData = snapshot.data; + return SeekBar( + duration: positionData?.duration ?? Duration.zero, + position: positionData?.position ?? Duration.zero, + bufferedPosition: positionData?.bufferedPosition ?? Duration.zero, + onChangeEnd: data.voiceController!.seek, + ).expanded; }, - ).expanded, + ), ], ), ).circle(5); @@ -354,7 +415,7 @@ class ChatBubble extends StatelessWidget { size: 30, color: MyColors.lightGreenColor, ).onPress(() { - provider.playVoice(context, data: modelData); + playVoice(context, data: modelData); }); } else if (processingState != ProcessingState.completed) { return Icon( @@ -362,7 +423,7 @@ class ChatBubble extends StatelessWidget { size: 30, color: MyColors.lightGreenColor, ).onPress(() { - provider.pausePlaying(context, data: modelData); + pausePlaying(context, data: modelData); }); } else { return Icon( @@ -370,7 +431,7 @@ class ChatBubble extends StatelessWidget { size: 30, color: MyColors.lightGreenColor, ).onPress(() { - player!.seek(Duration.zero); + rePlay(context, data: modelData); }); } }, @@ -378,64 +439,4 @@ class ChatBubble extends StatelessWidget { } } -class WaveBubble extends StatelessWidget { - final awf.PlayerController playerController; - final VoidCallback onTap; - final bool isPlaying; - - const WaveBubble({ - Key? key, - required this.playerController, - required this.onTap, - required this.isPlaying, - }) : super(key: key); - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.all(10), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - gradient: const LinearGradient( - transform: GradientRotation(.83), - begin: Alignment.topRight, - end: Alignment.bottomLeft, - colors: [ - MyColors.gradiantEndColor, - MyColors.gradiantStartColor, - ], - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: onTap, - icon: Icon(isPlaying ? Icons.stop : Icons.play_arrow), - color: Colors.white, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - awf.AudioFileWaveforms( - size: Size(MediaQuery.of(context).size.width / 2, 10), - playerController: playerController, - padding: EdgeInsets.zero, - margin: EdgeInsets.zero, - enableSeekGesture: true, - density: 1, - playerWaveStyle: const awf.PlayerWaveStyle( - fixedWaveColor: Colors.white, - liveWaveColor: MyColors.greenColor, - showTop: true, - showBottom: true, - waveCap: StrokeCap.round, - seekLineThickness: 2, - visualizerHeight: 4, - backgroundColor: Colors.transparent, - ), - ), - ], - ), - ); - } -} diff --git a/lib/ui/chat/chat_detailed_screen.dart b/lib/ui/chat/chat_detailed_screen.dart index c4c1eed..dcbda22 100644 --- a/lib/ui/chat/chat_detailed_screen.dart +++ b/lib/ui/chat/chat_detailed_screen.dart @@ -1,6 +1,4 @@ import 'dart:async'; -import 'dart:convert'; - import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -18,7 +16,7 @@ import 'package:mohem_flutter_app/models/chat/get_single_user_chat_list_model.da import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; import 'package:mohem_flutter_app/ui/chat/call/chat_outgoing_call_screen.dart'; import 'package:mohem_flutter_app/ui/chat/chat_bubble.dart'; -import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; +import 'package:mohem_flutter_app/ui/chat/common.dart'; import 'package:mohem_flutter_app/widgets/chat_app_bar_widge.dart'; import 'package:mohem_flutter_app/widgets/shimmer/dashboard_shimmer_widget.dart'; import 'package:provider/provider.dart'; diff --git a/lib/ui/chat/chat_home.dart b/lib/ui/chat/chat_home.dart index af243e5..8973f6b 100644 --- a/lib/ui/chat/chat_home.dart +++ b/lib/ui/chat/chat_home.dart @@ -81,6 +81,7 @@ class _ChatHomeState extends State { children: [ myTab(LocaleKeys.mychats.tr(), 0), myTab(LocaleKeys.favorite.tr(), 1), + myTab("My Team", 2), ], ), ), @@ -95,6 +96,7 @@ class _ChatHomeState extends State { children: [ ChatHomeScreen(), ChatFavoriteUsersScreen(), + ChatFavoriteUsersScreen(), ], ).expanded, ], diff --git a/lib/ui/chat/common.dart b/lib/ui/chat/common.dart new file mode 100644 index 0000000..e0cb4d0 --- /dev/null +++ b/lib/ui/chat/common.dart @@ -0,0 +1,189 @@ +import 'dart:math'; +import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; + +class SeekBar extends StatefulWidget { + final Duration duration; + final Duration position; + final Duration bufferedPosition; + final ValueChanged? onChanged; + final ValueChanged? onChangeEnd; + + const SeekBar({ + Key? key, + required this.duration, + required this.position, + required this.bufferedPosition, + this.onChanged, + this.onChangeEnd, + }) : super(key: key); + + @override + SeekBarState createState() => SeekBarState(); +} + +class SeekBarState extends State { + double? _dragValue; + late SliderThemeData _sliderThemeData; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _sliderThemeData = SliderTheme.of(context).copyWith( + // trackHeight: 2.0, + thumbColor: MyColors.lightGreenColor, + activeTrackColor: MyColors.lightGreenColor, + inactiveTrackColor: MyColors.grey57Color.withOpacity(0.4), + ); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + SliderTheme( + data: _sliderThemeData.copyWith( + thumbShape: HiddenThumbComponentShape(), + ), + child: ExcludeSemantics( + child: Slider( + min: 0.0, + max: widget.duration.inMilliseconds.toDouble(), + value: min(widget.bufferedPosition.inMilliseconds.toDouble(), widget.duration.inMilliseconds.toDouble()), + onChanged: (value) { + setState(() { + _dragValue = value; + }); + if (widget.onChanged != null) { + widget.onChanged!(Duration(milliseconds: value.round())); + } + }, + onChangeEnd: (value) { + if (widget.onChangeEnd != null) { + widget.onChangeEnd!(Duration(milliseconds: value.round())); + } + _dragValue = null; + }, + ), + ), + ), + SliderTheme( + data: _sliderThemeData.copyWith( + inactiveTrackColor: Colors.transparent, + ), + child: Slider( + min: 0.0, + max: widget.duration.inMilliseconds.toDouble(), + value: min(_dragValue ?? widget.position.inMilliseconds.toDouble(), widget.duration.inMilliseconds.toDouble()), + onChanged: (value) { + setState(() { + _dragValue = value; + }); + if (widget.onChanged != null) { + widget.onChanged!(Duration(milliseconds: value.round())); + } + }, + onChangeEnd: (value) { + if (widget.onChangeEnd != null) { + widget.onChangeEnd!(Duration(milliseconds: value.round())); + } + _dragValue = null; + }, + ), + ), + ], + ); + } +} + +class PositionData { + final Duration position; + final Duration bufferedPosition; + final Duration duration; + + PositionData(this.position, this.bufferedPosition, this.duration); +} + +class HiddenThumbComponentShape extends SliderComponentShape { + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) => Size.zero; + + @override + void paint( + PaintingContext context, + Offset center, { + required Animation activationAnimation, + required Animation enableAnimation, + required bool isDiscrete, + required TextPainter labelPainter, + required RenderBox parentBox, + required SliderThemeData sliderTheme, + required TextDirection textDirection, + required double value, + required double textScaleFactor, + required Size sizeWithOverflow, + }) {} +} + +class WaveBubble extends StatelessWidget { + final PlayerController playerController; + final VoidCallback onTap; + final bool isPlaying; + + const WaveBubble({ + Key? key, + required this.playerController, + required this.onTap, + required this.isPlaying, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + gradient: const LinearGradient( + transform: GradientRotation(.83), + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [ + MyColors.gradiantEndColor, + MyColors.gradiantStartColor, + ], + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: onTap, + icon: Icon(isPlaying ? Icons.stop : Icons.play_arrow), + color: Colors.white, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + AudioFileWaveforms( + size: Size(MediaQuery.of(context).size.width / 2, 10), + playerController: playerController, + padding: EdgeInsets.zero, + margin: EdgeInsets.zero, + enableSeekGesture: true, + density: 1, + playerWaveStyle: const PlayerWaveStyle( + fixedWaveColor: Colors.white, + liveWaveColor: MyColors.greenColor, + showTop: true, + showBottom: true, + waveCap: StrokeCap.round, + seekLineThickness: 2, + visualizerHeight: 4, + backgroundColor: Colors.transparent, + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 2f3fffa..e2092b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,9 +94,8 @@ dependencies: camera: ^0.10.0+4 #Chat Voice Message Recoding & Play -# record: ^4.4.3 audio_waveforms: ^0.1.5+1 -# animated_text_kit: ^4.2.2 + rxdart: ^0.27.7 #Encryption flutter_des: ^2.1.0 @@ -106,6 +105,7 @@ dependencies: safe_device: ^1.1.2 flutter_layout_grid: ^2.0.1 + dev_dependencies: flutter_test: sdk: flutter