diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index 563de659..cfead080 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -4,13 +4,13 @@ class URLs { static const String appReleaseBuildNumber = "28"; // static const host1 = "https://atomsm.hmg.com"; // production url - static const host1 = "https://atomsmdev.hmg.com"; // local DEV url - // static const host1 = "https://atomsmuat.hmg.com"; // local UAT url + // static const host1 = "https://atomsmdev.hmg.com"; // local DEV url + static const host1 = "https://atomsmuat.hmg.com"; // local UAT url // static const host1 = "http://10.201.111.125:9495"; // temporary Server UAT url - static String _baseUrl = "$_host/mobile"; + // static String _baseUrl = "$_host/mobile"; - // static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis + static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis // static final String _baseUrl = "$_host/v4/mobile"; // for asset inventory on UAT // static final String _baseUrl = "$_host/mobile"; // host local UAT // static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM diff --git a/lib/helper/utils.dart b/lib/helper/utils.dart index b56c7474..f4db3acd 100644 --- a/lib/helper/utils.dart +++ b/lib/helper/utils.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:google_api_availability/google_api_availability.dart'; + // import 'package:mohem_flutter_app/app_state/app_state.dart'; // import 'package:mohem_flutter_app/classes/colors.dart'; // import 'package:mohem_flutter_app/config/routes.dart'; @@ -23,6 +24,7 @@ import 'package:nfc_manager/platform_tags.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart'; import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart'; +import 'package:test_sa/views/widgets/dialogs/loading_dialog.dart'; // ignore_for_file: avoid_annotating_with_dynamic @@ -49,19 +51,21 @@ class Utils { return null; } } -static String getOrdinal(int number) { - if (number >= 11 && number <= 13) return "${number}th"; - switch (number % 10) { - case 1: - return "${number}st"; - case 2: - return "${number}nd"; - case 3: - return "${number}rd"; - default: - return "${number}th"; + + static String getOrdinal(int number) { + if (number >= 11 && number <= 13) return "${number}th"; + switch (number % 10) { + case 1: + return "${number}st"; + case 2: + return "${number}nd"; + case 3: + return "${number}rd"; + default: + return "${number}th"; + } } -} + static int stringToHex(String colorCode) { try { return int.parse(colorCode.replaceAll("#", "0xff")); @@ -73,7 +77,8 @@ static String getOrdinal(int number) { static Future delay(int millis) async { return await Future.delayed(Duration(milliseconds: millis)); } - static bool isBeforeOrEqualCurrentTime(TimeOfDay t1, TimeOfDay t2) { + + static bool isBeforeOrEqualCurrentTime(TimeOfDay t1, TimeOfDay t2) { return t1.hour < t2.hour || (t1.hour == t2.hour && t1.minute <= t2.minute); } @@ -102,6 +107,21 @@ static String getOrdinal(int number) { _isLoadingVisible = false; } + static void showUploadingDialog(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _isLoadingVisible = true; + + showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.5), + useRootNavigator: false, + builder: (BuildContext context) => const UploadingDialog(), + ).then((value) { + _isLoadingVisible = false; + }); + }); + } + static Future getStringFromPrefs(String key) async { SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getString(key) ?? ""; @@ -176,6 +196,7 @@ static String getOrdinal(int number) { ), ); } + // // static Widget getNoDataWidget(BuildContext context) { // return Column( @@ -421,15 +442,15 @@ static String getOrdinal(int number) { } return false; } - // - // static bool isDate(String input, String format) { - // try { - // DateTime d = DateFormat(format).parseStrict(input); - // //print(d); - // return true; - // } catch (e) { - // //print(e); - // return false; - // } - // } +// +// static bool isDate(String input, String format) { +// try { +// DateTime d = DateFormat(format).parseStrict(input); +// //print(d); +// return true; +// } catch (e) { +// //print(e); +// return false; +// } +// } } diff --git a/lib/modules/cm_module/views/forms/asset_retired/asset_retired.dart b/lib/modules/cm_module/views/forms/asset_retired/asset_retired.dart index 34416d06..e36e430a 100644 --- a/lib/modules/cm_module/views/forms/asset_retired/asset_retired.dart +++ b/lib/modules/cm_module/views/forms/asset_retired/asset_retired.dart @@ -55,8 +55,6 @@ class _AssetRetiredState extends State with TickerProviderStateMix @override Widget build(BuildContext context) { - - return Scaffold( key: _scaffoldKey, appBar: DefaultAppBar(title: context.translation.assetToBeRetired), @@ -125,9 +123,9 @@ class _AssetRetiredState extends State with TickerProviderStateMix onPressed: () async { requestDetailProvider.assetRetiredHelperModel?.activityAssetToBeRetiredAttachments = []; for (var item in _attachments) { - String fileName = ServiceRequestUtils.isLocalUrl(item.name??'') ? ("${item.name??''.split("/").last}|${base64Encode(File(item.name??'').readAsBytesSync())}") :item.name??''; - requestDetailProvider.assetRetiredHelperModel?.activityAssetToBeRetiredAttachments - ?.add(ActivityAssetToBeRetiredAttachments(id: item.id, name: fileName)); + String fileName = + ServiceRequestUtils.isLocalUrl(item.name ?? '') ? ("${item.name ?? ''.split("/").last}|${base64Encode(File(item.name ?? '').readAsBytesSync())}") : item.name ?? ''; + requestDetailProvider.assetRetiredHelperModel?.activityAssetToBeRetiredAttachments?.add(ActivityAssetToBeRetiredAttachments(id: item.id, name: fileName)); } int status = await requestDetailProvider.createActivityAssetToBeRetired(); if (status == 200) { diff --git a/lib/modules/cm_module/views/service_request_detail_main_view.dart b/lib/modules/cm_module/views/service_request_detail_main_view.dart index 65b93daf..1f2008d8 100644 --- a/lib/modules/cm_module/views/service_request_detail_main_view.dart +++ b/lib/modules/cm_module/views/service_request_detail_main_view.dart @@ -176,7 +176,7 @@ class _ServiceRequestDetailMainState extends State { void getChatToken(int moduleId, String title) { ChatProvider cProvider = Provider.of(context, listen: false); - if (cProvider.chatLoginResponse != null) return; + if (cProvider.chatLoginResponse != null && cProvider.referenceID == widget.requestId) return; String assigneeEmployeeNumber = Provider.of(context, listen: false).currentWorkOrder?.data?.assignedEmployee?.employeeId ?? ""; String myEmployeeId = context.userProvider.user!.username!; diff --git a/lib/modules/cx_module/chat/chat_api_client.dart b/lib/modules/cx_module/chat/chat_api_client.dart index 929f9946..37add788 100644 --- a/lib/modules/cx_module/chat/chat_api_client.dart +++ b/lib/modules/cx_module/chat/chat_api_client.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:test_sa/controllers/api_routes/api_manager.dart'; import 'package:test_sa/controllers/api_routes/urls.dart'; import 'package:test_sa/extensions/string_extensions.dart'; @@ -75,6 +76,16 @@ class ChatApiClient { } } + Future viewAllDocuments(int moduleId, int referenceId) async { + Response response = await ApiClient().getJsonForResponse("${URLs.chatHubUrlApi}/attachments/conversation?referenceId=$referenceId&moduleCode=$moduleId", token: chatLoginResponse!.token); + + if (response.statusCode == 200) { + return ChatParticipantModel.fromJson(jsonDecode(response.body)); + } else { + return null; + } + } + Future> loadChatHistory(int moduleId, int referenceId, String myId, String otherId) async { Response response = await ApiClient().postJsonForResponse( "${URLs.chatHubUrlApi}/UserChatHistory/GetUserChatHistory/$myId/$otherId/0", {"moduleCode": moduleId.toString(), "referenceId": referenceId.toString()}, @@ -203,26 +214,25 @@ class ChatApiClient { // } // Upload Chat Media -// Future uploadMedia(String userId, File file, String fileSource) async { -// if (kDebugMode) { -// print("${ApiConsts.chatMediaImageUploadUrl}upload"); -// print(AppState().chatDetails!.response!.token); -// } -// -// dynamic request = MultipartRequest('POST', Uri.parse('${ApiConsts.chatMediaImageUploadUrl}upload')); -// request.fields.addAll({'userId': userId, 'fileSource': fileSource}); -// request.files.add(await MultipartFile.fromPath('files', file.path)); -// request.headers.addAll({'Authorization': 'Bearer ${AppState().chatDetails!.response!.token}'}); -// StreamedResponse response = await request.send(); -// String data = await response.stream.bytesToString(); -// if (!kReleaseMode) { -// logger.i("res: " + data); -// } -// return jsonDecode(data); -// } + Future uploadMedia(String userId, File file, String fileSource, {Map? jsonData}) async { + dynamic request = MultipartRequest('POST', Uri.parse('${URLs.chatHubUrlApi}/attachments/upload')); + request.fields.addAll({'userId': userId, 'fileSource': fileSource}); + if (jsonData != null) { + request.fields.addAll(jsonData); + } -// Download File For Chat -// Future downloadURL({required String fileName, required String fileTypeDescription, required int fileSource}) async { + request.files.add(await MultipartFile.fromPath('file', file.path)); + request.headers.addAll({'Authorization': 'Bearer ${chatLoginResponse!.token}'}); + StreamedResponse response = await request.send(); + String data = await response.stream.bytesToString(); + if (!kReleaseMode) { + print("uploadMedia: $data"); + } + return jsonDecode(data); + } + +// // Download File For Chat +// Future downloadURL({required String fileName, required String fileTypeDescription, required int fileSource,required String url}) async { // Response response = await ApiClient().postJsonForResponse( // "${ApiConsts.chatMediaImageUploadUrl}download", // {"fileType": fileTypeDescription, "fileName": fileName, "fileSource": fileSource}, @@ -232,6 +242,83 @@ class ChatApiClient { // return data; // } +// Download File For Chat + Future downloadURL(String url, {required String fileName, required String fileTypeDescription, required int fileSource}) async { + Response response = await ApiClient().postJsonForResponse( + url, + {"fileType": fileTypeDescription, "fileName": fileName, "fileSource": fileSource}, + token: chatLoginResponse!.token, + ); + Uint8List data = Uint8List.fromList(response.bodyBytes); + return data; + } + + /// Downloads a file from a secure URL that uses a 302 redirect. + /// + /// [url]: The initial URL that requires the authorization token. + /// [token]: The Bearer token for authorization. + /// [saveFileName]: The name to give the downloaded file (e.g., 'my-document.pdf'). + Future downloadFileWithHttp(String url, {required String fileName, required String fileTypeDescription, required int fileSource}) async { + final client = http.Client(); + File? file; + try { + final request = http.Request('GET', Uri.parse(url)); + request.headers['Authorization'] = 'Bearer ${chatLoginResponse!.token}'; + + // This is the most important part: prevent automatic redirection. + request.followRedirects = false; + + print("Making initial request to: $url"); + final streamedResponse = await client.send(request); + + // --- Step 2: Check the response and handle the redirect --- + + // Check for a 302 redirect status. + if (streamedResponse.statusCode == 302) { + // Get the new URL from the 'Location' header. + final redirectUrl = streamedResponse.headers['location']; + + if (redirectUrl == null) { + throw Exception('302 Redirect did not contain a Location header.'); + } + + final fileResponse = await http.get(Uri.parse(redirectUrl), headers: {'Authorization': 'Bearer ${chatLoginResponse!.token}'}); + + if (fileResponse.statusCode == 200) { + // Get a safe directory to save the file. + final dir = await getTemporaryDirectory(); + file = File('${dir.path}/$fileName'); + + // Write the file to disk. + await file.writeAsBytes(fileResponse.bodyBytes); + print("File downloaded successfully and saved to: ${file.path}"); + } else { + // The download from the final URL failed. + throw Exception('Failed to download from redirect URL. Status: ${fileResponse.statusCode}'); + } + } + // Handle cases where the server might just send the file directly. + else if (streamedResponse.statusCode == 200) { + print("Server sent file directly without redirect."); + final dir = await getTemporaryDirectory(); + file = File('${dir.path}/$fileName'); + await file.writeAsBytes(await streamedResponse.stream.toBytes()); + print("File downloaded successfully and saved to: ${file.path}"); + } + // Handle other error statuses. + else { + throw Exception('Failed to initiate download. Status: ${streamedResponse.statusCode}'); + } + } catch (e) { + print("An error occurred during download: $e"); + } finally { + // Always close the client to free up resources. + client.close(); + } + + return file; + } + // //Get Chat Users & Favorite Images // Future> getUsersImages({required List encryptedEmails}) async { // List imagesData = []; diff --git a/lib/modules/cx_module/chat/chat_page.dart b/lib/modules/cx_module/chat/chat_page.dart index c1196027..4bab968c 100644 --- a/lib/modules/cx_module/chat/chat_page.dart +++ b/lib/modules/cx_module/chat/chat_page.dart @@ -1,6 +1,9 @@ import 'dart:convert'; +import 'dart:io'; import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/extensions/context_extension.dart'; @@ -8,15 +11,21 @@ import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/helper/utils.dart'; import 'package:test_sa/models/service_request/service_request.dart'; import 'package:test_sa/modules/cm_module/service_request_detail_provider.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cx_module/chat/chat_api_client.dart'; import 'package:test_sa/modules/cx_module/chat/chat_provider.dart'; +import 'package:test_sa/modules/cx_module/chat/view_all_attachment_page.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; +import 'package:test_sa/views/widgets/sound/sound_player.dart'; +import 'helper/chat_audio_player.dart'; +import 'helper/chat_file_picker.dart'; +import 'helper/chat_file_viewer.dart'; import 'model/get_single_user_chat_list_model.dart'; import 'model/user_chat_history_model.dart'; @@ -74,6 +83,7 @@ class _ChatPageState extends State { @override void dispose() { + chatHubConnection.stop(); playerController.dispose(); recorderController.dispose(); super.dispose(); @@ -113,14 +123,37 @@ class _ChatPageState extends State { Container( color: AppColor.neutral50, constraints: const BoxConstraints(maxHeight: 56), - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 8), + alignment: Alignment.center, child: Row( children: [ - Text( - chatProvider.recipient?.userName ?? "", - overflow: TextOverflow.ellipsis, - maxLines: 2, - style: AppTextStyles.bodyText2.copyWith(color: AppColor.white10), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + chatProvider.recipient?.userName ?? "", + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: AppTextStyles.bodyText2.copyWith(color: AppColor.white10), + ), + AnimatedSize( + duration: const Duration(milliseconds: 250), + child: SizedBox( + height: chatProvider.isTyping ? null : 0, + child: Text( + "Typing...", + maxLines: 1, + style: AppTextStyles.overline.copyWith(color: AppColor.white10), + ), + )), + // if (chatProvider.isTyping) + // Text( + // "Typing...", + // maxLines: 1, + // style: AppTextStyles.tinyFont2.copyWith(color: AppColor.white10), + // ), + ], ).expanded, 4.width, Text( @@ -130,7 +163,9 @@ class _ChatPageState extends State { decoration: TextDecoration.underline, decorationColor: AppColor.white10, ), - ), + ).onPress(() { + Navigator.push(context, CupertinoPageRoute(builder: (context) => ViewAllAttachmentPage(moduleId: 1, requestId: widget.requestId))); + }), ], ), ), @@ -196,8 +231,14 @@ class _ChatPageState extends State { maxLines: 3, textInputAction: TextInputAction.none, keyboardType: TextInputType.multiline, + onTap: () { + chatHubConnection.invoke("SendTypingAsync", args: [receiver]); + }, + onTapOutside: (PointerDownEvent event) { + chatHubConnection.invoke("SendStopTypingAsync", args: [receiver]); + }, onChanged: (text) { - chatHubConnection.invoke("SendTypingAsync", args: [context.userProvider.user!.username!]); + chatHubConnection.invoke("SendTypingAsync", args: [receiver]); }, decoration: InputDecoration( enabledBorder: InputBorder.none, @@ -214,7 +255,23 @@ class _ChatPageState extends State { ), ).expanded, IconButton( - onPressed: () {}, + onPressed: () async { + FocusScope.of(context).unfocus(); + File? file = (await showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + clipBehavior: Clip.antiAliasWithSaveLayer, + builder: (BuildContext context) => ChatFilePicker())) as File?; + if (file != null) { + Utils.showUploadingDialog(context); + await chatProvider.uploadAttachments(context.userProvider.user!.username!, file, "1"); + Utils.hideLoading(context); + } + }, style: const ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded ), @@ -458,24 +515,41 @@ class _ChatPageState extends State { splashColor: Colors.transparent, highlightColor: Colors.transparent, hoverColor: Colors.transparent, - onPressed: () { - chatProvider.invokeSendMessage({ - "Contant": textEditingController.text, - // "ContantNo": "0cc8b126-6180-4f91-a64d-2f62443b3f3f", - // "CreatedDate": "2025-11-09T18:58:12.502Z", - "CurrentEmployeeNumber": context.userProvider.user!.username!, - "ChatEventId": 1, - "ConversationId": chatProvider.chatParticipantModel!.id!.toString(), - "ModuleCode": widget.moduleId.toString(), - "ReferenceNumber": widget.requestId.toString(), - "UserChatHistoryLineRequestList": [ - {"TargetEmployeeNumber": receiver, "TargetUserStatus": 1, "IsSeen": false, "IsDelivered": true, "SeenOn": null, "DeliveredOn": null} - ] - }).then((success) { - if (success) { - textEditingController.clear(); + onPressed: () async { + if (chatState == ChatState.voiceRecordingCompleted) { + Utils.showUploadingDialog(context); + try { + await chatProvider.uploadAttachments(context.userProvider.user!.username!, File(recordedFilePath!), "1"); + Utils.hideLoading(context); + + await playerController.stopPlayer(); + recorderController.reset(); + recordedFilePath = null; + chatState = ChatState.idle; + setState(() {}); + } catch (ex) { + Utils.hideLoading(context); } - }); + } else { + if (textEditingController.text.isEmpty) return; + chatProvider.invokeSendMessage({ + "Contant": textEditingController.text, + // "ContantNo": "0cc8b126-6180-4f91-a64d-2f62443b3f3f", + // "CreatedDate": "2025-11-09T18:58:12.502Z", + "CurrentEmployeeNumber": context.userProvider.user!.username!, + "ChatEventId": 1, + "ConversationId": chatProvider.chatParticipantModel!.id!.toString(), + "ModuleCode": widget.moduleId.toString(), + "ReferenceNumber": widget.requestId.toString(), + "UserChatHistoryLineRequestList": [ + {"TargetEmployeeNumber": receiver, "TargetUserStatus": 1, "IsSeen": false, "IsDelivered": true, "SeenOn": null, "DeliveredOn": null} + ] + }).then((success) { + if (success) { + textEditingController.clear(); + } + }); + } }, style: const ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded @@ -555,10 +629,51 @@ class _ChatPageState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - chatResponse?.contant ?? msg, - style: AppTextStyles.bodyText2.copyWith(color: AppColor.neutral120), - ).toShimmer(context: context, isShow: loading), + if (chatResponse?.downloadUrl != null) ...[ + // if (chatResponse!.fileTypeResponse!.fileKind.toString().toLowerCase() == "audio") + // // ASoundPlayer(audio: chatResponse.downloadUrl!) + // + // ChatAudioPlayer(chatResponse!.fileTypeResponse!, chatResponse.downloadUrl!) + // // if ( + // // + // // chatResponse!.fileTypeResponse!.fileKind.toString().toLowerCase() == "image" + // // + // // ) + // + // else + ChatFileViewer(chatResponse!.downloadUrl!, chatResponse.fileTypeResponse!), + + // Container( + // width: 250, + // height: 250, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(12), + // border: Border.all(color: Colors.grey.withOpacity(.5), width: 1), + // image: DecorationImage( + // image: NetworkImage(chatResponse.downloadUrl!, headers: {'Authorization': "Bearer ${Provider.of(context, listen: false).chatLoginResponse!.token!}"}), + // ), + // ), + // // child: CachedNetworkImage( + // // imageUrl: chatResponse.downloadUrl! ?? "", + // // // fit: boxFit ?? BoxFit.cover, + // // // alignment: Alignment.center, + // // // width: width, + // // // height: height, + // // httpHeaders: {'Authorization': "Bearer ${Provider.of(context, listen: false).chatLoginResponse!.token!}"}, + // // placeholder: (context, url) => const Center(child: CircularProgressIndicator()), + // // // errorWidget: (context, url, error) => Icon(showDefaultIcon ? Icons.image_outlined : Icons.broken_image_rounded), + // // ), + // + // // Image.network( + // // chatResponse.downloadUrl!, + // // headers: {'Authorization': "Bearer ${Provider.of(context, listen: false).chatLoginResponse!.token!}"}, + // // ), + // ) + ] else + Text( + chatResponse?.contant ?? msg, + style: AppTextStyles.bodyText2.copyWith(color: AppColor.neutral120), + ).toShimmer(context: context, isShow: loading), if (loading) 4.height, Text( chatResponse?.createdDate?.toString().chatMsgTime ?? "2:00 PM", @@ -628,6 +743,9 @@ class _ChatPageState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ + if (chatResponse?.downloadUrl != null) ...[ + if (chatResponse!.fileTypeResponse!.fileKind.toString().toLowerCase() == "audio") ChatAudioPlayer(chatResponse!.fileTypeResponse!, chatResponse.downloadUrl!) + ], Text( contentMsg, style: AppTextStyles.bodyText2.copyWith(color: AppColor.white10), diff --git a/lib/modules/cx_module/chat/chat_provider.dart b/lib/modules/cx_module/chat/chat_provider.dart index 84384087..5ad5d1d5 100644 --- a/lib/modules/cx_module/chat/chat_provider.dart +++ b/lib/modules/cx_module/chat/chat_provider.dart @@ -106,6 +106,8 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { // // bool disbaleChatForThisUser = false; + bool isTyping = false; + bool chatLoginTokenLoading = false; ChatLoginResponse? chatLoginResponse; @@ -125,6 +127,10 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { Participants? sender; Participants? recipient; + late String receiverID; + late int moduleID; + int? referenceID; + void reset() { chatLoginTokenLoading = false; chatParticipantLoading = false; @@ -153,6 +159,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { Future getUserAutoLoginTokenSilent(int moduleId, int requestId, String title, String myId, String assigneeEmployeeNumber) async { reset(); + receiverID = assigneeEmployeeNumber; chatLoginTokenLoading = true; notifyListeners(); try { @@ -186,9 +193,17 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { // } catch (e) {} // } + Future viewAllDocuments(int moduleId, int requestId) async { + try { + await ChatApiClient().viewAllDocuments(moduleId, requestId); + } catch (ex) {} + } + Future connectToHub(int moduleId, int requestId, String myId, String assigneeEmployeeNumber) async { userChatHistoryLoading = true; notifyListeners(); + moduleID = moduleId; + referenceID = requestId; await buildHubConnection(chatParticipantModel!.id!.toString()); userChatHistory = await ChatApiClient().loadChatHistory(moduleId, requestId, myId, assigneeEmployeeNumber); chatResponseList = userChatHistory ?? []; @@ -239,22 +254,6 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { // return returnStatus; // } - void sendMsgSignalR() { - var abc = { - "Contant": "Follow-up: Test results look good.", - "ContantNo": "0cc8b126-6180-4f91-a64d-2f62443b3f3f", - "CreatedDate": "2025-11-09T18:58:12.502Z", - "CurrentEmployeeNumber": "EMP123456", - "ChatEventId": 1, - "ConversationId": "15521", - "ModuleCode": "CRM", - "ReferenceNumber": "CASE-55231", - "UserChatHistoryLineRequestList": [ - {"TargetEmployeeNumber": "EMP654321", "TargetUserStatus": 1, "IsSeen": false, "IsDelivered": true, "SeenOn": null, "DeliveredOn": null} - ] - }; - } - // List? uGroups = [], searchGroups = []; // Future getUserAutoLoginToken() async { @@ -287,6 +286,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { chatHubConnection.on("OnMessageReceivedAsync", onMsgReceived); chatHubConnection.on("OnSubmitChatAsync", OnSubmitChatAsync); chatHubConnection.on("OnTypingAsync", OnTypingAsync); + chatHubConnection.on("OnStopTypingAsync", OnStopTypingAsync); //group On message @@ -389,34 +389,28 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { // chatCID = uuid.v4(); // } - // void markRead(List data, int receiverID) { - // for (SingleUserChatModel element in data!) { - // if (AppState().chatDetails!.response!.id! == element.targetUserId) { - // if (element.isSeen != null) { - // if (!element.isSeen!) { - // element.isSeen = true; - // dynamic data = [ - // { - // "userChatHistoryId": element.userChatHistoryId, - // "TargetUserId": element.currentUserId == receiverID ? element.currentUserId : element.targetUserId, - // "isDelivered": true, - // "isSeen": true, - // } - // ]; - // updateUserChatHistoryStatusAsync(data); - // notifyListeners(); - // } - // } - // for (ChatUser element in searchedChats!) { - // if (element.id == receiverID) { - // element.unreadMessageCount = 0; - // chatUConvCounter = 0; - // } - // } - // } - // } - // notifyListeners(); - // } + void markRead(List data, String receiverID) { + for (SingleUserChatModel element in data) { + // if (AppState().chatDetails!.response!.id! == element.targetUserId) { + if (element.isSeen != null) { + if (!element.isSeen!) { + element.isSeen = true; + dynamic data = [ + { + "userChatHistoryId": element.userChatHistoryId, + "TargetUserId": element.currentUserId == receiverID ? element.currentUserId : element.targetUserId, + "isDelivered": true, + "isSeen": true, + } + ]; + updateUserChatHistoryStatusAsync(data); + notifyListeners(); + } + // } + } + } + // notifyListeners(); + } void updateUserChatHistoryStatusAsync(List data) { try { @@ -434,25 +428,51 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { } } - List getSingleUserChatModel(String str) => List.from(json.decode(str).map((x) => SingleUserChatModel.fromJson(x))); + // List getSingleUserChatModel(String str) => List.from(json.decode(str).map((x) => SingleUserChatModel.fromJson(x))); + List getSingleUserChatModel(String str) { + final dynamic decodedJson = json.decode(str); + + // Check if the decoded JSON is already a List + if (decodedJson is List) { + return List.from(decodedJson.map((x) => SingleUserChatModel.fromJson(x))); + } + // If it's a Map (a single object), wrap it in a list + else if (decodedJson is Map) { + return [SingleUserChatModel.fromJson(decodedJson)]; + } + // Handle unexpected types + else { + throw const FormatException('Expected a JSON object or a list of JSON objects.'); + } + } // List getGroupChatHistoryAsync(String str) => // List.from(json.decode(str).map((x) => groupchathistory.GetGroupChatHistoryAsync.fromJson(x))); // - // Future uploadAttachments(String userId, File file, String fileSource) async { - // dynamic result; - // try { - // Object? response = await ChatApiClient().uploadMedia(userId, file, fileSource); - // if (response != null) { - // result = response; - // } else { - // result = []; - // } - // } catch (e) { - // throw e; - // } - // return result; - // } + Future uploadAttachments(String userId, File file, String fileSource) async { + dynamic result; + try { + Map jsonData = { + "IsContextual": true.toString(), + "ModuleCode": moduleID.toString(), + "ReferenceId": referenceID.toString(), + "ReferenceType": "ticket", + "ConversationId": chatParticipantModel!.id.toString(), + "TargetUserId": receiverID, + "SendMessage": true.toString(), + }; + + Object? response = await ChatApiClient().uploadMedia(userId, file, fileSource, jsonData: jsonData); + if (response != null) { + result = response; + } else { + result = []; + } + } catch (e) { + throw e; + } + return result; + } // void updateUserChatStatus(List? args) { // dynamic items = args!.toList(); @@ -555,8 +575,27 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { // notifyListeners(); // } + Timer? timer; + Future OnTypingAsync(List? parameters) async { - print("OnTypingAsync:$parameters"); + // String empId = parameters!.first as String; + isTyping = true; + notifyListeners(); + if (timer?.isActive ?? false) { + timer!.cancel(); + } + timer = Timer(const Duration(milliseconds: 2500), () { + isTyping = false; + notifyListeners(); + }); + } + + Future OnStopTypingAsync(List? parameters) async { + if (timer?.isActive ?? false) { + timer!.cancel(); + } + isTyping = false; + notifyListeners(); } // Future OnSubmitChatAsync(List? parameters) async { @@ -632,11 +671,13 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { // ); // } // } - setMsgTune(); + // setMsgTune(); // userChatHistory = userChatHistory! + data; // chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!)); userChatHistory?.insert(0, data.first); notifyListeners(); + + // markRead(data, data.first.targetUserId!); // if (isChatScreenActive && data.first.currentUserId == receiverID) { // // } else { @@ -760,6 +801,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { // } void OnSubmitChatAsync(List? parameters) { + print("OnSubmitChatAsync:$parameters"); List data = []; for (dynamic msg in parameters!) { data = getSingleUserChatModel(jsonEncode(msg)); @@ -1215,7 +1257,7 @@ String getFileTypeDescription(String value) { // } // } // } - +// // void sendChatMessage( // BuildContext context, { // required int targetUserId, diff --git a/lib/modules/cx_module/chat/helper/chat_audio_player.dart b/lib/modules/cx_module/chat/helper/chat_audio_player.dart new file mode 100644 index 00000000..f32fcc06 --- /dev/null +++ b/lib/modules/cx_module/chat/helper/chat_audio_player.dart @@ -0,0 +1,138 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/modules/cx_module/chat/api_client.dart'; +import 'package:test_sa/modules/cx_module/chat/chat_api_client.dart'; +import 'package:test_sa/modules/cx_module/chat/model/get_single_user_chat_list_model.dart'; + +import '../../../../new_views/app_style/app_color.dart'; + +class ChatAudioPlayer extends StatefulWidget { + String downloadURl; + FileTypeResponse file; + bool isLocalFile; + + ChatAudioPlayer(this.file, this.downloadURl, {Key? key, this.isLocalFile = false}) : super(key: key); + + @override + _ChatAudioPlayerState createState() { + return _ChatAudioPlayerState(); + } +} + +class _ChatAudioPlayerState extends State { + bool downloading = false; + + PlayerController playerController = PlayerController(); + + @override + void initState() { + super.initState(); + checkFileInLocalStorage(); + } + + File? audioFile; + List waveformData = []; + + void checkFileInLocalStorage() async { + Directory tempDir = await getTemporaryDirectory(); + String tempPath = '${tempDir.path}/${widget.file.fileName}'; + File tempFile = File(tempPath); + bool exists = await tempFile.exists(); + if (exists) { + audioFile = tempFile; + playerController = PlayerController(); + await playerController.preparePlayer(path: tempPath); + waveformData = await playerController.extractWaveformData(path: tempPath); + } else { + downloadFile(); + } + } + + void downloadFile() async { + downloading = true; + setState(() {}); + try { + // Uint8List list = await ChatApiClient().downloadFileWithHttp(widget.downloadURl, fileName: widget.file.fileName, fileTypeDescription: widget.file.fileTypeDescription, fileSource: widget.file.fileTypeId!); + // audioFile = File.fromRawPath(list); + + audioFile = + await ChatApiClient().downloadFileWithHttp(widget.downloadURl, fileName: widget.file.fileName, fileTypeDescription: widget.file.fileTypeDescription, fileSource: widget.file.fileTypeId!); + + playerController = PlayerController(); + await playerController.preparePlayer(path: audioFile!.path); + waveformData = await playerController.extractWaveformData(path: audioFile!.path); + } catch (ex) { + print(ex); + audioFile = null; + } + downloading = false; + setState(() {}); + } + + @override + void dispose() { + playerController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 56, + child: Row( + children: [ + if (downloading) + const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 2), + ) + else if (playerController.playerState == PlayerState.playing) + IconButton( + onPressed: () async { + await playerController.pausePlayer(); + await playerController.stopPlayer(); + setState(() {}); + }, + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded + ), + icon: const Icon(Icons.stop_circle_outlined, size: 20), + constraints: const BoxConstraints(), + ) + else + IconButton( + onPressed: () async { + await playerController.startPlayer(); + setState(() {}); + }, + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, // or .padded + ), + icon: const Icon(Icons.play_circle_fill_rounded, size: 20), + constraints: const BoxConstraints(), + ), + AudioFileWaveforms( + playerController: playerController, + waveformData: waveformData, + enableSeekGesture: false, + continuousWaveform: false, + waveformType: WaveformType.long, + playerWaveStyle: const PlayerWaveStyle( + fixedWaveColor: AppColor.neutral50, + liveWaveColor: AppColor.primary10, + showSeekLine: true, + ), + size: Size(MediaQuery.of(context).size.width, 56.0), + ).expanded, + ], + ), + ); + } +} diff --git a/lib/modules/cx_module/chat/helper/chat_file_picker.dart b/lib/modules/cx_module/chat/helper/chat_file_picker.dart new file mode 100644 index 00000000..63e19d71 --- /dev/null +++ b/lib/modules/cx_module/chat/helper/chat_file_picker.dart @@ -0,0 +1,436 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:image_cropper/image_cropper.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/models/generic_attachment_model.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; + +class ChatFilePicker extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Attach File".heading4(context), + 12.height, + GridView( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 1, crossAxisSpacing: 12, mainAxisSpacing: 12), + children: [ + gridItem(Icons.camera_enhance_rounded, context.translation.pickFromCamera).onPress(() async { + await fromMediaPicker(context, ImageSource.camera); + }), + gridItem(Icons.image_rounded, context.translation.pickFromGallery).onPress(() async { + await fromMediaPicker(context, ImageSource.gallery); + }), + gridItem(Icons.file_present_rounded, context.translation.pickFromFiles).onPress(() async { + await fromFilePicker(context); + }), + ], + ), + 12.height, + ], + ).paddingAll(21); + } + + fromMediaPicker(BuildContext context, ImageSource imageSource) async { + XFile? pickedFile = await ImagePicker().pickImage(source: imageSource, imageQuality: 70, maxWidth: 800, maxHeight: 800); + if (pickedFile != null) { + CroppedFile? croppedFile = await ImageCropper().cropImage( + sourcePath: pickedFile.path, + aspectRatio: CropAspectRatio(ratioX: 1, ratioY: 1), + uiSettings: [ + AndroidUiSettings( + toolbarTitle: 'ATOMS', + toolbarColor: Colors.white, + toolbarWidgetColor: context.settingProvider.theme == "dark" ? AppColor.neutral10 : AppColor.neutral50, + initAspectRatio: CropAspectRatioPreset.square, + lockAspectRatio: false, + ), + IOSUiSettings(title: 'ATOMS'), + ], + ); + if (croppedFile != null) { + Navigator.pop(context, File(croppedFile.path)); + return; + } + } + Navigator.pop(context); + } + + fromFilePicker(BuildContext context) async { + FilePickerResult? result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xlsx', 'pptx'], + ); + if ((result?.paths ?? []).isNotEmpty) { + Navigator.pop(context, File(result!.paths.first!)); + } else { + Navigator.pop(context); + } + } + + Widget gridItem(IconData iconData, String title) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xffF1F1F1), width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon(iconData, color: const Color(0xff7D859A), size: 36), + Text( + title, + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + ), + ], + ), + ); + } + +// static Future selectFile(BuildContext context) async { +// ImageSource source = (await showModalBottomSheet( +// context: context, +// shape: const RoundedRectangleBorder( +// borderRadius: BorderRadius.vertical( +// top: Radius.circular(20), +// ), +// ), +// clipBehavior: Clip.antiAliasWithSaveLayer, +// builder: (BuildContext context) => ChatFilePicker())) as ImageSource; +// +// final pickedFile = await ImagePicker().pickImage(source: source, imageQuality: 70, maxWidth: 800, maxHeight: 800); +// } +} + +// class AttachmentPicker extends StatefulWidget { +// final String label; +// final bool error; +// final List attachment; +// +// final bool enabled, onlyImages; +// double? buttonHeight; +// Widget? buttonIcon; +// Color? buttonColor; +// final Function(List)? onChange; +// final bool showAsGrid; +// +// AttachmentPicker( +// {Key? key, +// this.attachment = const [], +// required this.label, +// this.error = false, +// this.buttonHeight, +// this.buttonIcon, +// this.enabled = true, +// this.onlyImages = false, +// this.onChange, +// this.showAsGrid = false, +// this.buttonColor}) +// : super(key: key); +// +// @override +// State createState() => _AttachmentPickerState(); +// } +// +// class _AttachmentPickerState extends State { +// @override +// Widget build(BuildContext context) { +// return Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// AppDashedButton( +// title: widget.label, +// height: widget.buttonHeight, +// buttonColor: widget.buttonColor, +// icon: widget.buttonIcon, +// onPressed: (widget.enabled == false) +// ? () {} +// : widget.showAsGrid +// ? showFileSourceSheet +// : onFilePicker), +// 16.height, +// if (widget.attachment.isNotEmpty) +// Wrap( +// spacing: 8.toScreenWidth, +// children: List.generate( +// widget.attachment.length, +// (index) { +// File image = File(widget.attachment[index].name!); +// return MultiFilesPickerItem( +// file: image, +// enabled: widget.enabled, +// onRemoveTap: (image) { +// if (!widget.enabled) { +// return; +// } +// widget.attachment.removeAt(index); +// if (widget.onChange != null) { +// widget.onChange!(widget.attachment); +// } +// setState(() {}); +// }, +// ); +// }, +// ), +// ), +// ], +// ); +// } +// +// fromFilePicker() async { +// FilePickerResult? result = await FilePicker.platform.pickFiles( +// type: FileType.custom, +// allowMultiple: true, +// allowedExtensions: widget.onlyImages ? ['jpg', 'jpeg', 'png'] : ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xlsx', 'pptx'], +// ); +// if (result != null) { +// for (var path in result.paths) { +// widget.attachment.add(GenericAttachmentModel(id: 0, name: File(path!).path)); +// } +// if (widget.onChange != null) { +// widget.onChange!(widget.attachment); +// } +// setState(() {}); +// } +// } +// +// void showFileSourceSheet() async { +// // if (widget.attachment.length >= 5) { +// // Fluttertoast.showToast(msg: context.translation.maxImagesNumberIs5); +// // return; +// // } +// +// ImageSource source = (await showModalBottomSheet( +// context: context, +// shape: const RoundedRectangleBorder( +// borderRadius: BorderRadius.vertical( +// top: Radius.circular(20), +// ), +// ), +// clipBehavior: Clip.antiAliasWithSaveLayer, +// builder: (BuildContext context) => Column( +// mainAxisSize: MainAxisSize.min, +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// "Attach File".heading4(context), +// 12.height, +// GridView( +// padding: const EdgeInsets.all(0), +// shrinkWrap: true, +// physics: const NeverScrollableScrollPhysics(), +// gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 1, crossAxisSpacing: 12, mainAxisSpacing: 12), +// children: [ +// gridItem(Icons.camera_enhance_rounded, context.translation.pickFromCamera).onPress(() => Navigator.of(context).pop(ImageSource.camera)), +// gridItem(Icons.image_rounded, context.translation.pickFromGallery).onPress(() => Navigator.of(context).pop(ImageSource.gallery)), +// gridItem(Icons.file_present_rounded, context.translation.pickFromFiles).onPress(() async { +// await fromFilePicker(); +// Navigator.pop(context); +// }), +// ], +// ), +// 12.height, +// ], +// ).paddingAll(21), +// )) as ImageSource; +// +// final pickedFile = await ImagePicker().pickImage(source: source, imageQuality: 70, maxWidth: 800, maxHeight: 800); +// +// if (pickedFile != null) { +// File fileImage = File(pickedFile.path); +// widget.attachment.add(GenericAttachmentModel(id: 0, name: fileImage.path)); +// if (widget.onChange != null) { +// widget.onChange!(widget.attachment); +// } +// setState(() {}); +// } +// } +// +// Widget gridItem(IconData iconData, String title) { +// return Container( +// padding: const EdgeInsets.all(12), +// decoration: BoxDecoration( +// color: Colors.white, +// borderRadius: BorderRadius.circular(12), +// border: Border.all(color: const Color(0xffF1F1F1), width: 1), +// ), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// Icon(iconData, color: const Color(0xff7D859A), size: 36), +// Text( +// title, +// style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), +// ), +// ], +// ), +// ); +// } +// +// onFilePicker() async { +// //TODO removed on request by Backend as they don't have anyissue with large number of files +// // if (widget.attachment.length >= 5) { +// // Fluttertoast.showToast(msg: context.translation.maxImagesNumberIs5); +// // return; +// // } +// ImageSource? source = await showModalBottomSheet( +// context: context, +// builder: (BuildContext context) { +// Widget listCard({required String icon, required String label, required VoidCallback onTap}) { +// return Container( +// padding: const EdgeInsets.all(12), +// decoration: BoxDecoration( +// color: AppColor.background(context), +// // color: Colors.white, +// borderRadius: BorderRadius.circular(12), +// border: Border.all(color: const Color(0xffF1F1F1), width: 1), +// ), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// icon.toSvgAsset(color: AppColor.iconColor(context), width: 36, height: 36), +// // Icon(iconData, color: const Color(0xff7D859A), size: 36), +// Text( +// label, +// style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), +// ), +// ], +// ), +// ).onPress(onTap); +// } +// +// return SafeArea( +// top: false, +// child: Container( +// width: double.infinity, +// color: AppColor.background(context), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// "Attach File".heading4(context), +// 12.height, +// GridView( +// padding: const EdgeInsets.all(0), +// shrinkWrap: true, +// physics: const NeverScrollableScrollPhysics(), +// gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 1, crossAxisSpacing: 12, mainAxisSpacing: 12), +// children: [ +// listCard( +// icon: 'camera_icon', +// label: '${context.translation.open}\n${context.translation.camera}', +// onTap: () { +// Navigator.of(context).pop(ImageSource.camera); +// }, +// ), +// listCard( +// icon: 'gallery_icon', +// label: '${context.translation.open}\n${context.translation.gallery}', +// onTap: () { +// Navigator.of(context).pop(ImageSource.gallery); +// }, +// ), +// listCard( +// icon: 'file_icon', +// label: '${context.translation.open}\n${context.translation.files}', +// onTap: () async { +// await fromFilePicker(); +// Navigator.pop(context); +// }, +// ), +// ], +// ), +// // Container( +// // padding: const EdgeInsets.all(16.0), +// // child: Row( +// // mainAxisAlignment: MainAxisAlignment.spaceBetween, +// // children: [ +// // listCard( +// // icon: 'camera_icon', +// // label: '${context.translation.open}\n${context.translation.camera}', +// // onTap: () { +// // Navigator.of(context).pop(ImageSource.camera); +// // }, +// // ), +// // listCard( +// // icon: 'gallery_icon', +// // label: '${context.translation.open}\n${context.translation.gallery}', +// // onTap: () { +// // Navigator.of(context).pop(ImageSource.gallery); +// // }, +// // ), +// // listCard( +// // icon: 'file_icon', +// // label: '${context.translation.open}\n${context.translation.files}', +// // onTap: () async { +// // await fromFilePicker(); +// // Navigator.pop(context); +// // }, +// // ), +// // ], +// // ), +// // ), +// ], +// ).paddingAll(16), +// ), +// ); +// }, +// ); +// // ImageSource source = await showDialog( +// // context: context, +// // builder: (dialogContext) => CupertinoAlertDialog( +// // actions: [ +// // TextButton( +// // child: Text(context.translation.pickFromCamera), +// // onPressed: () { +// // Navigator.of(dialogContext).pop(ImageSource.camera); +// // }, +// // ), +// // TextButton( +// // child: Text(context.translation.pickFromGallery), +// // onPressed: () { +// // Navigator.of(dialogContext).pop(ImageSource.gallery); +// // }, +// // ), +// // TextButton( +// // child: Text(context.translation.pickFromFiles), +// // onPressed: () async { +// // await fromFilePicker(); +// // Navigator.pop(context); +// // }, +// // ), +// // ], +// // ), +// // ); +// if (source == null) return; +// +// final pickedFile = await ImagePicker().pickImage(source: source, imageQuality: 70, maxWidth: 800, maxHeight: 800); +// +// if (pickedFile != null) { +// File fileImage = File(pickedFile.path); +// widget.attachment.add(GenericAttachmentModel(id: 0, name: fileImage.path)); +// if (widget.onChange != null) { +// widget.onChange!(widget.attachment); +// } +// setState(() {}); +// } +// +// setState(() {}); +// } +// } diff --git a/lib/modules/cx_module/chat/helper/chat_file_viewer.dart b/lib/modules/cx_module/chat/helper/chat_file_viewer.dart new file mode 100644 index 00000000..fbd53eec --- /dev/null +++ b/lib/modules/cx_module/chat/helper/chat_file_viewer.dart @@ -0,0 +1,137 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:open_file/open_file.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:provider/provider.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/modules/cx_module/chat/chat_api_client.dart'; +import 'package:test_sa/modules/cx_module/chat/chat_provider.dart'; +import 'package:test_sa/modules/cx_module/chat/model/get_single_user_chat_list_model.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/app_style/app_text_style.dart'; +import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; +import 'package:test_sa/views/widgets/images/multi_image_picker_item.dart'; +import 'package:test_sa/views/widgets/sound/sound_player.dart'; + +class ChatFileViewer extends StatelessWidget { + String downloadUrl; + FileTypeResponse fileTypeResponse; + + ChatFileViewer(this.downloadUrl, this.fileTypeResponse, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: checkFileInLocalStorage(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState != ConnectionState.waiting && snapshot.hasData) {} + + if (fileTypeResponse!.fileKind.toString().toLowerCase() == "audio") { + if (snapshot.connectionState == ConnectionState.waiting) { + return Container( + width: 48, + height: 48, + decoration: ShapeDecoration( + color: AppColor.background(context), + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: (context.isDark ? AppColor.neutral20 : AppColor.neutral30)), + borderRadius: BorderRadius.circular(32), + ), + ), + child: const CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 2).center, + ); + } + if (snapshot.data == null) { + return Text("Failed to load", style: AppTextStyle.tiny.copyWith(color: context.isDark ? AppColor.red50 : AppColor.red60)); + } + return ASoundPlayer(audio: snapshot.data!.path); + } + return Container( + width: 180, + height: 180, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.withOpacity(.5), width: 1), + image: snapshot.hasData ? DecorationImage(fit: BoxFit.contain, image: getImage(snapshot.data!)) : null, + ), + child: snapshot.connectionState == ConnectionState.waiting + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator(color: AppColor.primary10, strokeWidth: 2), + ).center + : snapshot.hasData + ? getFile(context, snapshot.data!) + : const Icon(Icons.broken_image_rounded), + ); + }); + } + + Widget getFile(BuildContext context, File file) { + bool isImage = file.path.split(".").last.toLowerCase() == "png" || file.path.split(".").last.toLowerCase() == "jpg" || file.path.split(".").last.toLowerCase() == "jpeg"; + bool isPdf = file.path.split(".").last.toLowerCase() == "pdf"; + bool isExcel = file.path.split(".").last.toLowerCase() == "xlsx"; + + return MaterialButton( + padding: EdgeInsets.zero, + onPressed: () async { + if (isImage) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => Scaffold( + appBar: const DefaultAppBar(), + body: SafeArea( + child: InteractiveViewer(child: Image.file(file)).center, + ), + ), + ), + ); + } else { + OpenFile.open(file.path); + } + // else { + // // if (!await launchUrl(Uri.parse(URLs.getFileUrl(file.path)!), mode: LaunchMode.externalApplication)) { + // // Fluttertoast.showToast(msg: "UnExpected Error with file."); + // // throw Exception('Could not launch'); + // // } + // } + }, + ); + } + + ImageProvider getImage(File file) { + bool isImage = file.path.split(".").last.toLowerCase() == "png" || file.path.split(".").last.toLowerCase() == "jpg" || file.path.split(".").last.toLowerCase() == "jpeg"; + bool isPdf = file.path.split(".").last.toLowerCase() == "pdf"; + bool isExcel = file.path.split(".").last.toLowerCase() == "xlsx"; + + if (isImage) { + return FileImage(file); + } + return AssetImage("assets/images/${isPdf ? "pdf" : isExcel ? "excel" : "doc"}.png"); + } + + Future checkFileInLocalStorage() async { + Directory tempDir = await getTemporaryDirectory(); + String tempPath = '${tempDir.path}/${fileTypeResponse.fileName}'; + File tempFile = File(tempPath); + bool exists = await tempFile.exists(); + if (exists) { + return tempFile; + } else { + return downloadFile(); + } + } + + Future downloadFile() async { + try { + return await ChatApiClient() + .downloadFileWithHttp(downloadUrl, fileName: fileTypeResponse.fileName, fileTypeDescription: fileTypeResponse.fileTypeDescription, fileSource: fileTypeResponse.fileTypeId!); + } catch (ex) { + print("downloadFile:$ex"); + return null; + } + } +} diff --git a/lib/modules/cx_module/chat/model/get_single_user_chat_list_model.dart b/lib/modules/cx_module/chat/model/get_single_user_chat_list_model.dart index 06e3da34..7d87aca2 100644 --- a/lib/modules/cx_module/chat/model/get_single_user_chat_list_model.dart +++ b/lib/modules/cx_module/chat/model/get_single_user_chat_list_model.dart @@ -29,6 +29,7 @@ class SingleUserChatModel { this.createdDate, this.chatSource, this.conversationId, + this.downloadUrl, this.fileTypeResponse, this.userChatReplyResponse, this.isReplied, @@ -56,6 +57,7 @@ class SingleUserChatModel { DateTime? createdDate; int? chatSource; String? conversationId; + String? downloadUrl; FileTypeResponse? fileTypeResponse; UserChatReplyResponse? userChatReplyResponse; bool? isReplied; @@ -84,6 +86,7 @@ class SingleUserChatModel { createdDate: json["createdDate"] == null ? null : DateTime.parse(json["createdDate"]), chatSource: json["chatSource"] == null ? null : json["chatSource"], conversationId: json["conversationId"] == null ? null : json["conversationId"], + downloadUrl: json["downloadUrl"] == null ? null : json["downloadUrl"], fileTypeResponse: json["fileTypeResponse"] == null ? null : FileTypeResponse.fromJson(json["fileTypeResponse"]), userChatReplyResponse: json["userChatReplyResponse"] == null ? null : UserChatReplyResponse.fromJson(json["userChatReplyResponse"]), isReplied: false, @@ -112,6 +115,7 @@ class SingleUserChatModel { "createdDate": createdDate == null ? null : createdDate!.toIso8601String(), "chatSource": chatSource == null ? null : chatSource, "conversationId": conversationId == null ? null : conversationId, + "downloadUrl": downloadUrl == null ? null : downloadUrl, "fileTypeResponse": fileTypeResponse == null ? null : fileTypeResponse!.toJson(), "userChatReplyResponse": userChatReplyResponse == null ? null : userChatReplyResponse!.toJson(), }; diff --git a/lib/modules/cx_module/chat/view_all_attachment_page.dart b/lib/modules/cx_module/chat/view_all_attachment_page.dart new file mode 100644 index 00000000..a92d9831 --- /dev/null +++ b/lib/modules/cx_module/chat/view_all_attachment_page.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/modules/cx_module/chat/chat_provider.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; + +class ViewAllAttachmentPage extends StatefulWidget { + int moduleId; + int requestId; + + ViewAllAttachmentPage({required this.moduleId, required this.requestId, Key? key}) : super(key: key); + + @override + _ViewAllAttachmentPageState createState() { + return _ViewAllAttachmentPageState(); + } +} + +class _ViewAllAttachmentPageState extends State { + @override + void initState() { + super.initState(); + Provider.of(context, listen: false).viewAllDocuments(widget.moduleId, widget.requestId); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColor.neutral100, + appBar: const DefaultAppBar(title: "Documents"), + body: FutureBuilder( + future: Provider.of(context, listen: false).viewAllDocuments(widget.moduleId, widget.requestId), + builder: (BuildContext context, AsyncSnapshot snapshot) { + bool isLoading = false; + if (snapshot.connectionState == ConnectionState.waiting) { + isLoading = true; + } + return GridView.builder( + itemCount: 9, //isLoading? 9: 2, + padding: const EdgeInsets.all(16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: context.isTablet() ? 4 : 3, childAspectRatio: 1, crossAxisSpacing: 8, mainAxisSpacing: 8), + itemBuilder: (BuildContext context, int index) { + return Container().toShimmer(context: context, isShow: isLoading); + }, + ); + }), + ); + } +} diff --git a/lib/views/widgets/dialogs/loading_dialog.dart b/lib/views/widgets/dialogs/loading_dialog.dart new file mode 100644 index 00000000..1a72e452 --- /dev/null +++ b/lib/views/widgets/dialogs/loading_dialog.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/views/app_style/sizing.dart'; + +class UploadingDialog extends StatelessWidget { + const UploadingDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + height: 150.toScreenWidth, + width: 200.toScreenWidth, + alignment: Alignment.center, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColor.neutral30, + borderRadius: BorderRadius.circular(20.0), + boxShadow: [AppStyle.boxShadow], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(strokeWidth: 3), + 16.height, + Text( + "Uploading...", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: AppColor.textColor(context), height: 35 / 24, letterSpacing: -0.96), + ) + ], + ), + ), + ); + } +} diff --git a/lib/views/widgets/sound/sound_player.dart b/lib/views/widgets/sound/sound_player.dart index edf8b377..a9368299 100644 --- a/lib/views/widgets/sound/sound_player.dart +++ b/lib/views/widgets/sound/sound_player.dart @@ -7,8 +7,9 @@ import 'package:test_sa/new_views/app_style/app_text_style.dart'; class ASoundPlayer extends StatefulWidget { final String audio; + final bool showLoading; - const ASoundPlayer({Key? key, required this.audio}) : super(key: key); + const ASoundPlayer({Key? key, required this.audio, this.showLoading = false}) : super(key: key); @override _ASoundPlayerState createState() => _ASoundPlayerState(); @@ -161,6 +162,7 @@ class _ASoundPlayerState extends State { if (_isLocalFile) { await _audioPlayer.setSourceDeviceFile(_audio); } else { + // await _audioPlayer.setSource(UrlSource(_audio, headers: widget.headers)); await _audioPlayer.setSourceUrl(_audio); } _audioPlayer.seek(const Duration(milliseconds: 0));