diff --git a/lib/api/chat/chat_api_client.dart b/lib/api/chat/chat_api_client.dart index 5bceb24..409295f 100644 --- a/lib/api/chat/chat_api_client.dart +++ b/lib/api/chat/chat_api_client.dart @@ -33,11 +33,12 @@ class ChatApiClient { }, ); if (!kReleaseMode) { + print("Status Code is ================" + response.statusCode.toString()); logger.i("res: " + response.body); } if (response.statusCode == 200) { userLoginResponse = user.userAutoLoginModelFromJson(response.body); - } else if (response.statusCode == 504) { + } else if (response.statusCode == 501 || response.statusCode == 502 || response.statusCode == 503 || response.statusCode == 504) { getUserLoginToken(); } else { userLoginResponse = user.userAutoLoginModelFromJson(response.body); @@ -141,11 +142,16 @@ class ChatApiClient { // Download File For Chat Future downloadURL({required String fileName, required String fileTypeDescription}) async { + print(fileName); + print(fileTypeDescription); + print("${ApiConsts.chatMediaImageUploadUrl}download"); + print(AppState().chatDetails!.response!.token); Response response = await ApiClient().postJsonForResponse( "${ApiConsts.chatMediaImageUploadUrl}download", {"fileType": fileTypeDescription, "fileName": fileName, "fileSource": 1}, token: AppState().chatDetails!.response!.token, ); + Uint8List data = Uint8List.fromList(response.bodyBytes); return data; } @@ -159,11 +165,10 @@ class ChatApiClient { ); if (!kReleaseMode) { logger.i("res: " + response.body); - print("Images Status Coe is ============== " + response.statusCode.toString()); } if (response.statusCode == 200) { imagesData = chatUserImageModelFromJson(response.body); - } else if (response.statusCode == 504 || response.statusCode == 500) { + } else if (response.statusCode == 500 || response.statusCode == 504) { getUsersImages(encryptedEmails: encryptedEmails); } else { Utils.showToast("Something went wrong while loading images"); diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index 0559996..c147ea3 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -3,8 +3,8 @@ import 'package:mohem_flutter_app/ui/marathon/widgets/question_card.dart'; class ApiConsts { //static String baseUrl = "http://10.200.204.20:2801/"; // Local server // static String baseUrl = "https://erptstapp.srca.org.sa"; // SRCA server - static String baseUrl = "https://uat.hmgwebservices.com"; // UAT server - // static String baseUrl = "https://hmgwebservices.com"; // Live server + //static String baseUrl = "https://uat.hmgwebservices.com"; // UAT server + static String baseUrl = "https://hmgwebservices.com"; // Live server static String baseUrlServices = baseUrl + "/Services/"; // server // static String baseUrlServices = "https://api.cssynapses.com/tangheem/"; // Live server static String utilitiesRest = baseUrlServices + "Utilities.svc/REST/"; diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index a62016f..5f28b0b 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -2,18 +2,20 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:http/http.dart'; -import 'package:just_audio/just_audio.dart'; +import 'package:just_audio/just_audio.dart' as JustAudio; 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/app_permissions.dart'; import 'package:mohem_flutter_app/classes/consts.dart'; import 'package:mohem_flutter_app/classes/encryption.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/main.dart'; -import 'package:mohem_flutter_app/models/chat/chat_count_conversation_model.dart'; import 'package:mohem_flutter_app/models/chat/chat_user_image_model.dart'; import 'package:mohem_flutter_app/models/chat/get_search_user_chat_model.dart'; import 'package:mohem_flutter_app/models/chat/get_single_user_chat_list_model.dart'; @@ -23,6 +25,7 @@ import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart'; import 'package:mohem_flutter_app/widgets/image_picker.dart'; import 'package:open_file/open_file.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:signalr_netcore/hub_connection.dart'; import 'package:signalr_netcore/signalr_client.dart'; import 'package:uuid/uuid.dart'; @@ -37,7 +40,6 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { bool isLoading = true; bool isChatScreenActive = false; int receiverID = 0; - late File selectedFile; bool isFileSelected = false; String sFileType = ""; @@ -53,9 +55,13 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { Future getUserAutoLoginToken() async { userLoginToken.UserAutoLoginModel userLoginResponse = await ChatApiClient().getUserLoginToken(); + print("======================================= Chat Login Token Check ====================================="); + logger.d(userLoginResponse.toJson()); + print("======================================= Chat Login Token Check ====================================="); if (userLoginResponse.response != null) { AppState().setchatUserDetails = userLoginResponse; } else { + AppState().setchatUserDetails = userLoginResponse; Utils.showToast( userLoginResponse.errorResponses!.first.fieldName.toString() + " Erorr", ); @@ -903,7 +909,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void setMsgTune() async { - AudioPlayer player = AudioPlayer(); + JustAudio.AudioPlayer player = JustAudio.AudioPlayer(); await player.setVolume(1.0); String audioAsset = ""; if (Platform.isAndroid) { @@ -949,4 +955,133 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { logger.d([reciptUser, currentUser]); await chatHubConnection.invoke("UserTypingAsync", args: [reciptUser, currentUser]); } + + // Audio Recoding Work + Timer? _timer; + int _recodeDuration = 0; + bool isRecoding = false; + bool isPause = false; + bool isPlaying = false; + String? path; + String? musicFile; + late Directory appDirectory; + late RecorderController recorderController; + late PlayerController playerController; + + //////// Audio Recoding Work //////////////////// + + Future initAudio() async { + appDirectory = await getApplicationDocumentsDirectory(); + path = "${appDirectory.path}/${AppState().chatDetails!.response!.id}-${DateTime.now().microsecondsSinceEpoch}.aac"; + recorderController = RecorderController() + ..androidEncoder = AndroidEncoder.aac + ..androidOutputFormat = AndroidOutputFormat.mpeg4 + ..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC + ..sampleRate = 8000 + ..bitRate = 32000; + playerController = PlayerController(); + } + + void disposeAudio() { + isRecoding = false; + isPlaying = false; + isPause = false; + recorderController.dispose(); + playerController.dispose(); + } + + void startRecoding() async { + PermissionStatus status = await Permission.microphone.request(); + print(status); + if (status.isDenied == true) { + startRecoding(); + } else { + recorderController.reset(); + await recorderController.record(path); + _recodeDuration = 0; + _startTimer(); + isRecoding = !isRecoding; + notifyListeners(); + } + } + + void _startTimer() { + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { + _recodeDuration++; + buildTimer(); + notifyListeners(); + }); + } + + Future pauseRecoding() async { + isPause = true; + isPlaying = true; + recorderController.pause(); + path = await recorderController.stop(false); + print(path); + File file = File(path!); + file.readAsBytesSync(); + await playerController.preparePlayer(file.path, 1.0); + var tempDuration = _recodeDuration; + _recodeDuration = tempDuration; + _timer?.cancel(); + notifyListeners(); + } + + void resumeRecoding() { + isPause = false; + isPlaying = false; + isRecoding = true; + recorderController.record(path); + _startTimer(); + } + + Future deleteRecoding() async { + print(path); + _recodeDuration = 0; + _timer?.cancel(); + // path = await recorderController.stop(false); + recorderController.reset(); + print(path); + if (path != null && path!.isNotEmpty) { + File delFile = File(path!); + double fileSizeInKB = delFile.lengthSync() / 1024; + double fileSizeInMB = fileSizeInKB / 1024; + debugPrint("Deleted file size: ${delFile.lengthSync()}"); + debugPrint("Deleted file size in KB: " + fileSizeInKB.toString()); + debugPrint("Deleted file size in MB: " + fileSizeInMB.toString()); + if (await delFile.exists()) { + delFile.delete(); + } + isPause = false; + isRecoding = false; + isPlaying = false; + notifyListeners(); + } + } + + String buildTimer() { + String minutes = _formatNum(_recodeDuration ~/ 60); + String seconds = _formatNum(_recodeDuration % 60); + return '$minutes : $seconds'; + } + + String _formatNum(int number) { + String numberStr = number.toString(); + if (number < 10) { + numberStr = '0' + numberStr; + } + return numberStr; + } + + void playRecoding() async { + isPlaying = true; + await playerController.startPlayer(finishMode: FinishMode.stop); + } + + void playOrPause() async { + playerController.playerState == PlayerState.playing ? await playerController.pausePlayer() : playRecoding(); + notifyListeners(); + } } diff --git a/lib/ui/chat/chat_bubble.dart b/lib/ui/chat/chat_bubble.dart index 8978ff1..06afecf 100644 --- a/lib/ui/chat/chat_bubble.dart +++ b/lib/ui/chat/chat_bubble.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:mohem_flutter_app/api/api_client.dart'; @@ -120,10 +121,14 @@ class ChatBubble extends StatelessWidget { else Row( children: [ - if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 || fileTypeID == 2) + if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 + // || fileTypeID == 2 + ) SvgPicture.asset(data.getType(fileTypeName ?? ""), height: 30, width: 22, alignment: Alignment.center, fit: BoxFit.cover).paddingOnly(left: 0, right: 10), (cItem.contant ?? "").toText12().expanded, - if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 || fileTypeID == 2) const Icon(Icons.remove_red_eye, size: 16) + if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 + //|| fileTypeID == 2 + ) const Icon(Icons.remove_red_eye, size: 16) ], ), Align( @@ -217,10 +222,15 @@ class ChatBubble extends StatelessWidget { else Row( children: [ - if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 || fileTypeID == 2) + if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 + // || fileTypeID == 2 + ) SvgPicture.asset(data.getType(fileTypeName ?? ""), height: 30, width: 22, alignment: Alignment.center, fit: BoxFit.cover).paddingOnly(left: 0, right: 10), (cItem.contant ?? "").toText12(color: Colors.white).expanded, - if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 || fileTypeID == 2) const Icon(Icons.remove_red_eye, color: Colors.white, size: 16) + if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 + //|| fileTypeID == 2 + ) + const Icon(Icons.remove_red_eye, color: Colors.white, size: 16) ], ), Align( @@ -274,3 +284,63 @@ class ChatBubble extends StatelessWidget { } } } + +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, + playerWaveStyle: const PlayerWaveStyle( + fixedWaveColor: Colors.white, + liveWaveColor:MyColors.lightGreenColor, + showTop: true, + showBottom: true, + waveCap: StrokeCap.round, + seekLineThickness: 3, + visualizerHeight: 6, + backgroundColor: Colors.transparent + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/chat/chat_detailed_screen.dart b/lib/ui/chat/chat_detailed_screen.dart index 379fe2b..5d3648e 100644 --- a/lib/ui/chat/chat_detailed_screen.dart +++ b/lib/ui/chat/chat_detailed_screen.dart @@ -1,6 +1,7 @@ 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'; import 'package:flutter_svg/flutter_svg.dart'; @@ -62,6 +63,12 @@ class _ChatDetailScreenState extends State { _rc.loadComplete(); } + @override + void dispose() { + data.disposeAudio(); + super.dispose(); + } + @override Widget build(BuildContext context) { params = ModalRoute.of(context)!.settings.arguments as ChatDetailedScreenParams; @@ -73,6 +80,7 @@ class _ChatDetailScreenState extends State { loadMore: false, isNewChat: params!.isNewChat!, ); + data.initAudio(); } return Scaffold( @@ -181,62 +189,140 @@ class _ChatDetailScreenState extends State { height: 1, color: MyColors.lightGreyEFColor, ), - TextField( - controller: m.message, - decoration: InputDecoration( - hintText: m.isFileSelected ? m.selectedFile.path.split("/").last : LocaleKeys.typeheretoreply.tr(), - hintStyle: TextStyle(color: m.isFileSelected ? MyColors.darkTextColor : MyColors.grey98Color, fontSize: 14), - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - filled: true, - fillColor: MyColors.white, - contentPadding: const EdgeInsets.only( - left: 21, - top: 20, - bottom: 20, - ), - prefixIconConstraints: const BoxConstraints(), - prefixIcon: m.sFileType.isNotEmpty - ? SvgPicture.asset(m.getType(m.sFileType), height: 30, width: 22, alignment: Alignment.center, fit: BoxFit.cover).paddingOnly(left: 21, right: 15) - : null, - suffixIcon: SizedBox( - width: 100, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, // added line - children: [ - if (m.sFileType.isNotEmpty) - Row( - children: [ - const Icon(Icons.cancel, size: 15, color: MyColors.redA3Color).paddingOnly(right: 5), - ("Clear").toText11(color: MyColors.redA3Color, isUnderLine: true).paddingOnly(left: 0), - ], - ).onPress(() => m.removeAttachment()).paddingOnly(right: 25), - if (m.sFileType.isEmpty) - RotationTransition( - turns: const AlwaysStoppedAnimation(45 / 360), - child: const Icon(Icons.attach_file_rounded, size: 26, color: MyColors.grey3AColor).onPress( - () => m.selectImageToUpload(context), + if (m.isRecoding) + Column( + children: [ + Row( + children: [ + Text(m.buildTimer()).paddingAll(10), + if (m.isRecoding && m.isPlaying) + WaveBubble( + playerController: m.playerController, + onTap: () { + m.playOrPause(); + }, + isPlaying: m.playerController.playerState == PlayerState.playing) + .expanded + else + AudioWaveforms( + waveStyle: const WaveStyle( + waveColor: MyColors.lightGreenColor, + middleLineColor: Colors.transparent, + extendWaveform: true, + showBottom: true, + showTop: true, + waveThickness: 2, + showMiddleLine: false, + middleLineThickness: 0, ), - ).paddingOnly(right: 25), - SvgPicture.asset("assets/icons/chat/chat_send_icon.svg", height: 26, width: 26).onPress( + padding: const EdgeInsets.all(5), + shouldCalculateScrolledPosition: false, + margin: EdgeInsets.zero, + size: const Size(double.infinity, 30.0), + recorderController: m.recorderController, + backgroundColor: Colors.white, + ).expanded, + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Icon( + Icons.delete_outlined, + size: 26, + color: MyColors.lightGreenColor, + ).paddingAll(10).onPress(() { + m.deleteRecoding(); + }), + if (m.isPause) + const Icon( + Icons.mic, + size: 26, + color: MyColors.lightGreenColor, + ).paddingOnly(right: 15).onPress(() { + m.resumeRecoding(); + }), + if (!m.isPause) + const Icon( + Icons.pause_circle_outline, + size: 26, + color: MyColors.lightGreenColor, + ).paddingOnly(right: 15).onPress(() { + m.pauseRecoding(); + }), + SvgPicture.asset("assets/icons/chat/chat_send_icon.svg", height: 26, width: 26) + .onPress( + () => m.sendChatMessage(context, + targetUserId: params!.chatUser!.id!, + userStatus: params!.chatUser!.userStatus ?? 0, + userEmail: params!.chatUser!.email!, + targetUserName: params!.chatUser!.userName!), + ) + .paddingOnly(right: 21), + ], + ), + ], + ).objectContainerView(disablePadding: true, radius: 0), + if (!m.isRecoding) + Row( + children: [ + TextField( + controller: m.message, + decoration: InputDecoration( + hintText: m.isFileSelected ? m.selectedFile.path.split("/").last : LocaleKeys.typeheretoreply.tr(), + hintStyle: TextStyle(color: m.isFileSelected ? MyColors.darkTextColor : MyColors.grey98Color, fontSize: 14), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + filled: true, + fillColor: MyColors.white, + contentPadding: const EdgeInsets.only( + left: 21, + top: 20, + bottom: 20, + ), + prefixIconConstraints: const BoxConstraints(), + prefixIcon: m.sFileType.isNotEmpty + ? SvgPicture.asset(m.getType(m.sFileType), height: 30, width: 22, alignment: Alignment.center, fit: BoxFit.cover).paddingOnly(left: 21, right: 15) + : null, + ), + onChanged: (val) { + m.userTypingInvoke(currentUser: AppState().chatDetails!.response!.id!, reciptUser: params!.chatUser!.id!); + }, + ).expanded, + if (m.sFileType.isNotEmpty) + Row( + children: [ + const Icon(Icons.cancel, size: 15, color: MyColors.redA3Color).paddingOnly(right: 5), + ("Clear").toText11(color: MyColors.redA3Color, isUnderLine: true).paddingOnly(left: 0), + ], + ).onPress(() => m.removeAttachment()).paddingOnly(right: 15), + if (m.sFileType.isEmpty) + RotationTransition( + turns: const AlwaysStoppedAnimation(45 / 360), + child: const Icon(Icons.attach_file_rounded, size: 26, color: MyColors.grey3AColor).onPress( + () => m.selectImageToUpload(context), + ), + ).paddingOnly(right: 15), + Icon( + Icons.mic, + color: MyColors.lightGreenColor, + ).paddingOnly(right: 15).onPress(() { + m.startRecoding(); + }), + SvgPicture.asset("assets/icons/chat/chat_send_icon.svg", height: 26, width: 26) + .onPress( () => m.sendChatMessage(context, targetUserId: params!.chatUser!.id!, userStatus: params!.chatUser!.userStatus ?? 0, userEmail: params!.chatUser!.email!, targetUserName: params!.chatUser!.userName!), - ), - ], - ), - ).paddingOnly(right: 21), - ), - onChanged: (val) { - m.userTypingInvoke(currentUser: AppState().chatDetails!.response!.id!, reciptUser: params!.chatUser!.id!); - }, - ), + ) + .paddingOnly(right: 21), + ], + ).objectContainerView(disablePadding: true, radius: 0), ], )); }, diff --git a/pubspec.yaml b/pubspec.yaml index baa5e9f..c0b77bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -92,6 +92,10 @@ dependencies: swipe_to: ^1.0.2 flutter_webrtc: ^0.9.16 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 #Encryption