import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:onufitness/constants/api_endpoints.dart'; import 'package:onufitness/constants/api_enum_constant.dart'; import 'package:onufitness/screens/echoboard/models/get_exclusive_connections_response_model.dart'; import 'package:onufitness/screens/echoboard/models/poll_submit_response_model.dart'; import 'package:onufitness/screens/echoboard/models/post_model.dart'; import 'package:onufitness/services/api_services/base_api_services.dart'; import 'package:onufitness/utils/custom_cache_manager.dart'; import 'package:onufitness/utils/custom_sneakbar.dart'; import 'package:video_player/video_player.dart'; class EchoBoardController extends GetxController { // Privacy edit state final selectedPrivacyId = 0.obs; final isUpdatingPrivacy = false.obs; final RxString currentEditingPostId = ''.obs; // Exclusive connections for privacy edit ExclusiveConnectionResponseModel privacyExclusiveConnections = ExclusiveConnectionResponseModel(); final isPrivacyConnectionsLoading = false.obs; final isPrivacyConnectionPaginationLoading = false.obs; final privacyConnectionCurrentPage = 1.obs; final privacyConnectionPageSize = 15; final hasMorePrivacyConnections = true.obs; final privacyConnectionSearchValue = ''.obs; TextEditingController privacyConnectionSearchController = TextEditingController(); // Track selected connections for privacy final RxList selectedPrivacyConnectionIds = [].obs; final RxList> selectedPrivacyConnections = >[].obs; // Track original state for comparison final RxList originalPrivacyConnectionIds = [].obs; //.......................................................................................... // Initialize privacy edit state //.......................................................................................... void initializePrivacyEdit(SocialPostItem post) { selectedPrivacyId.value = post.postVisibilityId ?? ApiEnum.publicPostVisibilityID; currentEditingPostId.value = post.postId.toString(); // Reset connection states - THIS IS CORRECT for initial load selectedPrivacyConnectionIds.clear(); selectedPrivacyConnections.clear(); originalPrivacyConnectionIds.clear(); privacyConnectionSearchValue.value = ''; privacyConnectionSearchController.clear(); privacyConnectionCurrentPage.value = 1; hasMorePrivacyConnections.value = true; privacyExclusiveConnections = ExclusiveConnectionResponseModel(); } //.......................................................................................... // Select privacy option //.......................................................................................... void selectPrivacy(int privacyId) { selectedPrivacyId.value = privacyId; } //.......................................................................................... // Fetch exclusive connections for a specific post //.......................................................................................... Future fetchExclusiveConnectionsForPost( int postId, { bool refresh = false, }) async { if (refresh) { privacyConnectionCurrentPage.value = 1; hasMorePrivacyConnections.value = true; privacyExclusiveConnections = ExclusiveConnectionResponseModel(); } else { if (isPrivacyConnectionPaginationLoading.value || !hasMorePrivacyConnections.value) { return false; } } if (refresh) { isPrivacyConnectionsLoading.value = true; } else { isPrivacyConnectionPaginationLoading.value = true; } final userSearchValue = privacyConnectionSearchValue.value.trim(); try { final url = userSearchValue.isEmpty ? "${ApiUrl.getExclusiveConnectionsByPost}/$postId?PageNumber=${privacyConnectionCurrentPage.value}&PageSize=$privacyConnectionPageSize" : "${ApiUrl.getExclusiveConnectionsByPost}/$postId?PageNumber=${privacyConnectionCurrentPage.value}&PageSize=$privacyConnectionPageSize&SearchType=name&SearchValue=$userSearchValue"; final response = await ApiBase.getRequest(extendedURL: url); if (response.statusCode == 200 || response.statusCode == 201) { final newData = exclusiveConnectionResponseModelFromJson(response.body); if (newData.isSuccess == true && newData.data != null) { if (refresh) { privacyExclusiveConnections = newData; // This preserves user selections during search if (originalPrivacyConnectionIds.isEmpty) { for (var connection in newData.data?.items ?? []) { if (connection.isAdded == true) { final id = connection.connectedUserId != null ? 'user_${connection.connectedUserId}' : 'tribe_${connection.tribeId}'; selectedPrivacyConnectionIds.add(id); originalPrivacyConnectionIds.add(id); selectedPrivacyConnections.add({ "TribeID": connection.tribeId, "ConnectedUserID": connection.connectedUserId, }); } } } } else { final currentItems = privacyExclusiveConnections.data?.items ?? []; final newItems = newData.data?.items ?? []; privacyExclusiveConnections.data?.items = [ ...currentItems, ...newItems, ]; privacyExclusiveConnections.data?.totalCount = newData.data?.totalCount; } final fetchedCount = privacyExclusiveConnections.data?.items?.length ?? 0; final totalCount = newData.data?.totalCount ?? 0; hasMorePrivacyConnections.value = fetchedCount < totalCount; if (newData.data?.items?.isNotEmpty == true) { privacyConnectionCurrentPage.value++; } else { hasMorePrivacyConnections.value = false; } return true; } } } catch (e) { log("Error fetching exclusive connections for post: $e"); } finally { isPrivacyConnectionsLoading.value = false; isPrivacyConnectionPaginationLoading.value = false; } return false; } //.......................................................................................... void searchPrivacyConnections(int postId) { privacyConnectionCurrentPage.value = 1; hasMorePrivacyConnections.value = true; // ✅ Don't clear selections when searching fetchExclusiveConnectionsForPost(postId, refresh: true); } //.......................................................................................... // ✅ UPDATED: Clear search - preserve selections //.......................................................................................... void clearPrivacySearch() { privacyConnectionSearchValue.value = ''; privacyConnectionSearchController.clear(); final postId = int.tryParse(currentEditingPostId.value); if (postId != null) { // ✅ Don't clear selections when clearing search searchPrivacyConnections(postId); } } //.......................................................................................... // Toggle connection selection //.......................................................................................... void togglePrivacyConnection(ExclusiveConnections connection) { final isUser = connection.connectedUserId != null; final id = isUser ? 'user_${connection.connectedUserId}' : 'tribe_${connection.tribeId}'; if (selectedPrivacyConnectionIds.contains(id)) { // Unselect selectedPrivacyConnectionIds.remove(id); selectedPrivacyConnections.removeWhere((element) { return (connection.tribeId != null && element['TribeID'] == connection.tribeId) || (connection.connectedUserId != null && element['ConnectedUserID'] == connection.connectedUserId); }); } else { // Select selectedPrivacyConnectionIds.add(id); selectedPrivacyConnections.add({ "TribeID": connection.tribeId, "ConnectedUserID": connection.connectedUserId, }); } } //.......................................................................................... // Update post privacy //.......................................................................................... Future updatePostPrivacy(int postId) async { isUpdatingPrivacy.value = true; try { // Determine which connections were added and removed final newConnections = >[]; final deletedConnections = >[]; // If privacy is exclusive, calculate differences if (selectedPrivacyId.value == ApiEnum.exclusivePostVisibilityID) { // Find newly added connections for (var connection in selectedPrivacyConnections) { final id = connection['ConnectedUserID'] != null ? 'user_${connection['ConnectedUserID']}' : 'tribe_${connection['TribeID']}'; if (!originalPrivacyConnectionIds.contains(id)) { newConnections.add(connection); } } // Find removed connections for (var originalId in originalPrivacyConnectionIds) { if (!selectedPrivacyConnectionIds.contains(originalId)) { // Parse the ID to get the connection details if (originalId.startsWith('user_')) { final userId = originalId.replaceFirst('user_', ''); deletedConnections.add({ "TribeID": null, "ConnectedUserID": userId, }); } else if (originalId.startsWith('tribe_')) { final tribeId = int.tryParse( originalId.replaceFirst('tribe_', ''), ); deletedConnections.add({ "TribeID": tribeId, "ConnectedUserID": null, }); } } } } final body = { "PostID": postId, "PostVisibilityID": selectedPrivacyId.value, "IsTribe": "false", "TribeID": "0", "NewPostExclusiveConnectionJson": jsonEncode(newConnections), "DeletePostExclusiveConnectionJson": jsonEncode(deletedConnections), }; log("Update privacy request body: ${body.toString()}"); var response = await ApiBase.patchAnyFileWithMimeType( extendedURL: ApiUrl.updatePost, body: body, file: null, fileNameKey: null, ); log("Update privacy response: ${response.body}"); if (response.statusCode == 200 || response.statusCode == 201) { final responseData = jsonDecode(response.body); if (responseData['isSuccess'] == true) { // Update the post in cache with new privacy settings final post = getPostFromCache(postId.toString()); if (post != null) { post.postVisibilityId = selectedPrivacyId.value; updatePostInCache(post); } Future.delayed(Duration(milliseconds: 100), () { customSnackbar( title: "Success", message: "Post privacy updated successfully", duration: 2, ); }); refreshPosts(); return true; } } customSnackbar( title: "Error", message: "Failed to update post privacy", duration: 2, ); return false; } catch (e) { log("Error updating post privacy: $e"); customSnackbar( title: "Error", message: "Failed to update post privacy", duration: 2, ); return false; } finally { isUpdatingPrivacy.value = false; } } //..................................................................................................... @override void onInit() async { captionCtrl = TextEditingController(); connectionSearchController = TextEditingController(); privacyConnectionSearchController = TextEditingController(); super.onInit(); // fetchPosts(refresh: true); } @override void onClose() { captionCtrl.dispose(); videoController.value?.dispose(); privacyConnectionSearchController.dispose(); super.onClose(); } //.......................................................................................... //.......................................................................................... // fetchPosts API call..................................................................... //.......................................................................................... //.......................................................................................... final posts = [].obs; final isLoading = true.obs; final currentPage = 1.obs; final pageSize = 15.obs; final hasMoreData = true.obs; final searchType = ''.obs; final searchValue = ''.obs; // Cache map for storing posts with their IDs as keys final RxMap postsCache = {}.obs; // For video preview Rxn videoController = Rxn(); final isVideoInitialized = false.obs; final isVideoPlaying = false.obs; PostModel postModel = PostModel(); RxBool isPostFetchedLoading = false.obs; final isPaginationLoading = false.obs; Future fetchPosts({bool refresh = false, int tribeId = 0}) async { log( "fetchPosts called with refresh: $refresh, currentPage: ${currentPage.value}", ); // Check if we need to exit early if (!refresh) { if (isPaginationLoading.value) { log("Pagination already in progress, skipping fetch"); return false; } if (hasMoreData.value == false) { log("No more data available, skipping fetch"); return false; } } // Set loading states if (refresh) { currentPage.value = 1; hasMoreData.value = true; posts.clear(); postsCache.clear(); isPostFetchedLoading.value = true; } else { isPaginationLoading.value = true; } try { final url = tribeId != 0 ? "/api/Socials/get-tribe-posts/$tribeId?PageNumber=${currentPage.value}&PageSize=${pageSize.value}" : "${ApiUrl.getPosts}?PageNumber=${currentPage.value}&PageSize=${pageSize.value}&SearchType=${searchType.value}&SearchValue=${searchValue.value}"; log("Fetching posts with URL: $url"); final response = await ApiBase.getRequest(extendedURL: url); postModel = postModelFromJson(response.body); if (response.statusCode == 200 && postModel.isSuccess == true) { final newItems = postModel.data?.items ?? []; log("Fetched ${newItems.length} new items"); //.....Below one is the Previous One........................................... // if (newItems.isNotEmpty) { // for (var post in newItems) { // if (post.postId != null) { // postsCache[post.postId.toString()] = post; // } // } // posts.addAll(newItems); // // Increment page number for next fetch // currentPage.value++; // log("Updated current page to: ${currentPage.value}"); // } //......Below one is For Image Optimization....................................... if (newItems.isNotEmpty) { for (var post in newItems) { if (post.postId != null) { postsCache[post.postId.toString()] = post; } final imageUrl = getFirstImageUrl(post); if (imageUrl != null && imageUrl.isNotEmpty && _isValidNetworkUrl(imageUrl)) { WidgetsBinding.instance.addPostFrameCallback((_) { final context1 = Get.context; if (context1 != null && context1.mounted) { precacheImage( CachedNetworkImageProvider( imageUrl, cacheManager: CustomCacheManager.instance, ), context1, ); } }); } } posts.addAll(newItems); currentPage.value++; log("Updated current page to: ${currentPage.value}"); } //........................................................ if (newItems.isEmpty) { log("No more data available, setting hasMoreData to false"); hasMoreData.value = false; } return true; } else { return false; } } catch (e) { log("Exception in fetchPosts: $e"); } finally { isPostFetchedLoading.value = false; isPaginationLoading.value = false; log( "Loading states reset: isPostFetchedLoading=${isPostFetchedLoading.value}, isPaginationLoading=${isPaginationLoading.value}", ); } isPostFetchedLoading(false); return false; } bool _isValidNetworkUrl(String url) { try { final uri = Uri.parse(url); // Check if it's a valid HTTP/HTTPS URL with a host return (uri.scheme == 'http' || uri.scheme == 'https') && uri.host.isNotEmpty; } catch (e) { log("Invalid URL format: $url, error: $e"); return false; } } Future refreshPosts() async { log("Refreshing posts"); await fetchPosts(refresh: true); } // Get a post from cache by ID....................................................................................... SocialPostItem? getPostFromCache(String id) { return postsCache[id]; } // Add or update a post in the cache................................................................................. void updatePostInCache(SocialPostItem post) { if (post.postId != null) { postsCache[post.postId.toString()] = post; // Also update in the main posts list if it exists there final index = posts.indexWhere((p) => p.postId == post.postId); if (index != -1) { posts[index] = post; // Force UI update posts.refresh(); } } } Future initializeVideoPlayer(String videoPath) async { videoController.value?.dispose(); final controller = VideoPlayerController.file(File(videoPath)); videoController.value = controller; await controller.initialize(); await controller.setLooping(true); isVideoInitialized.value = true; toggleVideoPlayback(); } void toggleVideoPlayback() { if (!isVideoInitialized.value) return; if (isVideoPlaying.value) { videoController.value?.pause(); } else { videoController.value?.play(); } isVideoPlaying.value = !isVideoPlaying.value; } // Utility method to check if a post has media bool hasMedia(SocialPostItem post) { return (post.mediaFiles != null && post.mediaFiles!.isNotEmpty); } // Utility method to check if a post has a video bool hasVideo(SocialPostItem post) { if (post.mediaFiles == null || post.mediaFiles!.isEmpty) return false; return post.mediaFiles!.any( (media) => media.mediaType != null && media.mediaType!.toLowerCase().contains('video'), ); } // Utility method to check if a post has an image bool hasImage(SocialPostItem post) { if (post.mediaFiles == null || post.mediaFiles!.isEmpty) return false; return post.mediaFiles!.any( (media) => media.mediaType != null && media.mediaType!.toLowerCase().contains('image'), ); } // Utility method to check if a post has a poll bool hasPoll(SocialPostItem post) { return post.poll != null; } // Utility method to get the first image URL if exists String? getFirstImageUrl(SocialPostItem post) { if (!hasImage(post)) return null; final imageMedia = post.mediaFiles!.firstWhere( (media) => media.mediaType != null && media.mediaType!.toLowerCase().contains('image'), orElse: () => MediaFile(), ); return imageMedia.mediaFile; } // Utility method to get the first video URL if exists String? getFirstVideoUrl(SocialPostItem post) { if (!hasVideo(post)) return null; final videoMedia = post.mediaFiles!.firstWhere( (media) => media.mediaType != null && media.mediaType!.toLowerCase().contains('video'), orElse: () => MediaFile(), ); return videoMedia.mediaFile; } //.................................................................................................................................................................................................. //........ Share Post Feature................................................................................................. //.................................................................................................................................................................................................. final RxBool isLoadingSharedPost = false.obs; Future loadSpecificPost({ required String postId, int tribeId = 0, }) async { log('Loading specific post: $postId'); isLoadingSharedPost.value = true; try { // Find the existing index before removing (to maintain position if post exists) final existingIndex = posts.indexWhere( (p) => p.postId.toString() == postId, ); // Remove from list and cache to ensure fresh data posts.removeWhere((p) => p.postId.toString() == postId); postsCache.remove(postId); // Always fetch from API log('Fetching post from API'); final url = tribeId != 0 ? "${ApiUrl.getSinglePostByID}/$postId?tribeId=$tribeId&pageNumber=1&pageSize=10" : "${ApiUrl.getSinglePostByID}/$postId?pageNumber=1&pageSize=10"; final response = await ApiBase.getRequest(extendedURL: url); log("Specific post API statuscode: ${response.statusCode}"); log("Specific post API body: ${response.body}"); log("Specific post API url: $url"); if (response.statusCode == 200 || response.statusCode == 201) { final postData = jsonDecode(response.body); if (postData['isSuccess'] == true && postData['data'] != null) { final SocialPostItem specificPost = SocialPostItem.fromJson( postData['data'], ); // Add to cache postsCache[postId] = specificPost; // Insert at the same position if it existed, otherwise at the beginning if (existingIndex != -1 && existingIndex < posts.length) { posts.insert(existingIndex, specificPost); } else { posts.insert(0, specificPost); } log('Specific post loaded successfully'); return true; } else { log('API returned success=false or no data'); showPostNotFoundError(); return false; } } else { log('Failed to load specific post - Status: ${response.statusCode}'); showPostNotFoundError(); return false; } } catch (e) { log('Error loading specific post: $e'); showPostNotFoundError(); return false; } finally { isLoadingSharedPost.value = false; } } void showPostNotFoundError() { customSnackbar( title: "Error", message: "Could not load the post.", duration: 2, ); } //....................................................................................................................... //................................................................................................................................................ // Poll........................................................................................................................................... //................................................................................................................................................ Future submitPollVote( int pollId, int optionId, ) async { try { log("Submitting poll vote - PollID: $pollId, OptionID: $optionId"); final response = await ApiBase.postRequest( extendedURL: "${ApiUrl.submitPollVote}?PollID=$pollId&OptionID=$optionId", body: {}, ); log("Poll vote response status code: ${response.statusCode}"); if (response.statusCode == 200) { final result = pollSubmitResponseModelFromJson(response.body); // Update the post in our data source updatePostWithPollResults(result, selectedOptionId: optionId); return result; } else { log("Poll vote error: ${response.body}"); customSnackbar( title: "Error", message: "Something went wrong while voting", ); } } catch (e) { log("Poll vote exception: $e"); customSnackbar( title: "Error", message: "Something went wrong while voting", ); } return null; } //............................................................................................................. // Replace ( Update) Post with poll results.................................................................... //............................................................................................................ void updatePostWithPollResults( PollSubmitResponseModel response, { int? selectedOptionId, }) { // Find the post that contains this poll final int index = posts.indexWhere( (post) => post.poll?.pollPostId == response.data!.pollPostId, ); if (index != -1) { log("Updating post at index $index with new poll results"); // Create a deep copy of the post to ensure reactivity final SocialPostItem post = posts[index]; final SocialPostItem updatedPost = SocialPostItem( postId: post.postId, userId: post.userId, content: post.content, mediaFiles: post.mediaFiles, profilePicture: post.profilePicture, fullName: post.fullName, userTypeName: post.userTypeName, createdAt: post.createdAt, totalPostReactions: post.totalPostReactions, totalPostComments: post.totalPostComments, postReactionTypeId: post.postReactionTypeId, ); // Create a copy of the poll if (post.poll != null) { updatedPost.poll = Poll( pollPostId: post.poll!.pollPostId, question: post.poll!.question, expiryDate: post.poll!.expiryDate, totalPollVotes: response.data!.totalPollVotes, options: [], // Will add options below ); // Add options with updated vote counts if (post.poll!.options != null) { for (var oldOption in post.poll!.options!) { final responseOption = response.data!.options?.firstWhere( (o) => o.optionId == oldOption.optionId, orElse: () { return PollSubmitResponseOptions( optionId: oldOption.optionId, totalOptionPollVotes: oldOption.totalOptionPollVotes, ); }, ); updatedPost.poll!.options!.add( Option( optionId: oldOption.optionId, pollId: oldOption.pollId, userId: oldOption.userId, optionLabel: oldOption.optionLabel, totalOptionPollVotes: responseOption?.totalOptionPollVotes ?? oldOption.totalOptionPollVotes, percentageOptionPollVotes: oldOption.percentageOptionPollVotes, hasUserVoted: responseOption?.hasUserVoted ?? (selectedOptionId == oldOption.optionId), ), ); } } } log("Selected option------>: $selectedOptionId"); // Update the post in the list immediately posts[index] = updatedPost; posts.refresh(); // Refresh the UI update(); } } //............................................................................................................ //............................................................................................................ //........................................................ final mediaType = Rx(MediaType.none); final _picker = ImagePicker(); Future pickImage() async { final image = await _picker.pickImage(source: ImageSource.gallery); if (image != null) { final extension = image.path.split('.').last.toUpperCase(); final allowedExtensions = ['JPG', 'JPEG', 'PNG']; final file = File(image.path); final fileSize = await file.length(); const maxFileSize = 5 * 1024 * 1024; if (!allowedExtensions.contains(extension)) { customSnackbar( title: 'Unsupported Format', message: 'Please select a JPG, PNG image.', ); return; } if (fileSize > maxFileSize) { customSnackbar( title: "File Too Large", message: 'Image must be 5MB or smaller. Please select a smaller file.', ); return; } clearVideo(); selectedImage.value = file; mediaType.value = MediaType.image; } } Future pickVideo() async { final video = await _picker.pickVideo( source: ImageSource.gallery, maxDuration: const Duration(minutes: 5), ); if (video != null) { final extension = video.path.split('.').last.toUpperCase(); final allowedExtensions = ['MP4', 'M4V']; if (!allowedExtensions.contains(extension)) { customSnackbar( title: 'Unsupported Format', message: 'Please select a valid video file.', ); return; } final file = File(video.path); final fileSize = await file.length(); const maxFileSize = 50 * 1024 * 1024; if (fileSize > maxFileSize) { customSnackbar( title: "File Too Large", message: 'Video must be 50MB or smaller. Please select a smaller file.', ); return; } selectedImage.value = null; selectedVideo.value = file; mediaType.value = MediaType.video; await initializeVideoPlayer(video.path); } } void clearVideo() { selectedVideo.value = null; videoController.value?.pause(); videoController.value?.dispose(); videoController.value = null; isVideoInitialized.value = false; isVideoPlaying.value = false; mediaType.value = MediaType.none; } void removeMedia() { selectedImage.value = null; clearVideo(); } //......Upload a Post ..................................................................... final selectedImage = Rxn(); final selectedVideo = Rxn(); TextEditingController captionCtrl = TextEditingController(); var captionText = ''.obs; final visibility = 'Public'.obs; final List> visibilityTypes = [ { 'label': 'Public', 'publicId': ApiEnum.publicPostVisibilityID, 'description': 'Anyone can see', 'icon': Icons.public, }, { 'label': 'Private', 'privateId': ApiEnum.privatePostVisibilityID, 'description': 'Share with me', 'icon': Icons.lock, }, { 'label': 'Connections', 'description': 'Selected connections', 'icon': Icons.people, 'trailing': Icon(Icons.chevron_right), "exclusiveId": ApiEnum.exclusivePostVisibilityID, }, ]; RxBool isPostUploadLoading = false.obs; Future uploadPost({int tribeIdForTribeEchoboard = 0}) async { isPostUploadLoading(true); bool ret = false; try { File? fileToUpload; XFile? nullableFileToUpload; bool hasMedia = false; //....................................................................... if (mediaType.value == MediaType.image && selectedImage.value != null) { fileToUpload = selectedImage.value!; hasMedia = true; } else if (mediaType.value == MediaType.video && selectedVideo.value != null) { fileToUpload = selectedVideo.value!; hasMedia = true; } //....................................................................... if (hasMedia && fileToUpload != null) { nullableFileToUpload = XFile(fileToUpload.path); } //....................................................................... Map body = {}; if (tribeIdForTribeEchoboard != 0) { body = { "Content": captionCtrl.text, "PostTypeID": hasMedia ? mediaType.value == MediaType.image ? ApiEnum.imagePostTypeID : ApiEnum.videoPostTypeID : ApiEnum.onlyTextPostTypeId, "TribeID": tribeIdForTribeEchoboard, "DeviceTypeID": Platform.isAndroid ? ApiEnum.androidDeviceTypeID : ApiEnum.iosDeviceTypeID, }; } if (tribeIdForTribeEchoboard == 0) { body = { "Content": captionCtrl.text, "PostTypeID": hasMedia ? mediaType.value == MediaType.image ? ApiEnum.imagePostTypeID : ApiEnum.videoPostTypeID : ApiEnum.onlyTextPostTypeId, "PostVisibilityID": getSelectedVisibilityId(), "TribeID": 0, "DeviceTypeID": Platform.isAndroid ? ApiEnum.androidDeviceTypeID : ApiEnum.iosDeviceTypeID, "ExclusiveConnectionsJson": jsonEncode( selectedConnections, ), // need to send in string format that's why "jsonEncode" used ( backend requirement ) }; } final url = tribeIdForTribeEchoboard == 0 ? ApiUrl.uploadSocialPost : ApiUrl.uploadTribeEchoboardPost; var response = await ApiBase.postAnyFileWithMimeType( extendedURL: url, body: body, file: nullableFileToUpload, fileNameKey: hasMedia ? "PostMediaFile" : null, ); //....................................................................... if (response.statusCode == 200 || response.statusCode == 201) { log("Successful"); ret = true; resetAfterSuccessfulUpload(); customSnackbar( title: "Success !", message: "Post upload successful", duration: 1, ); } else { ret = false; customSnackbar( title: "Failed !", message: "Post upload failed, please try again..", duration: 1, ); } } catch (e) { customSnackbar( title: "Failed !", message: "Post upload failed, please try again..", duration: 1, ); } finally { isPostUploadLoading(false); } return ret; } //............................................................................. // Helper method to reset all variables after successful upload............... void resetAfterSuccessfulUpload() { captionCtrl.clear(); selectedImage.value = null; selectedVideo.value = null; // Reset video controller if it exists if (videoController.value != null) { videoController.value!.pause(); videoController.value!.dispose(); videoController.value = null; isVideoInitialized.value = false; isVideoPlaying.value = false; } mediaType.value = MediaType.none; visibility.value = 'Public'; } //.............................................................................. // Get Selected Visibility from API Enum......................................... String getSelectedVisibilityId() { final selectedVisibility = visibilityTypes.firstWhere( (element) => element['label'] == visibility.value, orElse: () => visibilityTypes.first, ); if (selectedVisibility.containsKey('publicId')) { return selectedVisibility['publicId'].toString(); } else if (selectedVisibility.containsKey('privateId')) { return selectedVisibility['privateId'].toString(); } else if (selectedVisibility.containsKey('exclusiveId')) { return selectedVisibility['exclusiveId'].toString(); } return ApiEnum.publicPostVisibilityID.toString(); } //.......................................................................................... //.......................................................................................... //.............Connection Fetch (Exclusive)................................................. //.......................................................................................... //.......................................................................................... final RxList> selectedConnections = >[].obs; final RxList selectedIds = [].obs; RxString connectionSearchValue = ''.obs; RxInt connectionCurrentPage = 1.obs; final int connectionPageSize = 15; final hasMoreDataConnection = true.obs; final isConnectionPaginationLoading = false.obs; TextEditingController connectionSearchController = TextEditingController(); ExclusiveConnectionResponseModel exclusiveConnections = ExclusiveConnectionResponseModel(); RxBool isConnectionsFetchLoading = false.obs; Future fetchExclusiveConnections({bool refresh = false}) async { if (refresh) { connectionCurrentPage.value = 1; hasMoreDataConnection.value = true; exclusiveConnections = ExclusiveConnectionResponseModel(); } else { if (isConnectionPaginationLoading.value || hasMoreDataConnection.value == false) { log("Pagination already loading or no more data"); return false; } } log( "Fetching connections: page ${connectionCurrentPage.value}, search: ${connectionSearchValue.value}", ); isConnectionPaginationLoading(true); if (refresh) isConnectionsFetchLoading(true); try { final response = await ApiBase.getRequest( extendedURL: "${ApiUrl.fetchExclusiveConnections}?pageNumber=${connectionCurrentPage.value}&pagesize=$connectionPageSize&searchType=name&searchValue=${connectionSearchValue.value}", ); if (response.statusCode == 200 || response.statusCode == 201) { final newData = exclusiveConnectionResponseModelFromJson(response.body); if (newData.isSuccess == true && newData.data != null) { if (refresh) { exclusiveConnections = newData; } else { // Append to existing items final currentItems = exclusiveConnections.data?.items ?? []; final newItems = newData.data?.items ?? []; exclusiveConnections.data?.items = [...currentItems, ...newItems]; exclusiveConnections.data?.totalCount = newData.data?.totalCount; } // Check if we've fetched all available items final fetchedItemsCount = exclusiveConnections.data?.items?.length ?? 0; final totalItemsCount = newData.data?.totalCount ?? 0; // Update pagination status - if we have fewer items than total, there's more to load hasMoreDataConnection.value = fetchedItemsCount < totalItemsCount; // Increment page for next fetch if this fetch was successful and had items if (newData.data?.items?.isNotEmpty == true) { connectionCurrentPage.value++; } else { // No more items to fetch hasMoreDataConnection.value = false; } return true; } else { customSnackbar( title: "Error", message: "Failed to fetch connection details", ); return false; } } else { customSnackbar( title: "Error", message: "Failed to fetch connection details", ); return false; } } catch (e) { log("Error fetching connections: $e"); return false; } finally { isConnectionsFetchLoading(false); isConnectionPaginationLoading(false); } } //.......................................................................................... void clearSearch() { connectionSearchValue.value = ''; connectionCurrentPage.value = 1; connectionSearchController.clear(); fetchExclusiveConnections(refresh: true); } //.......................................................................................... void exclusiveConnectionToggleSelection({ required int? tribeId, required String? connectedUserId, }) { final String id = tribeId != null ? 'tribe_$tribeId' : 'user_$connectedUserId'; if (selectedIds.contains(id)) { // Unselect..................... selectedIds.remove(id); selectedConnections.removeWhere((element) { return (tribeId != null && element['TribeID'] == tribeId) || (connectedUserId != null && element['ConnectedUserID'] == connectedUserId); }); } else { // Select........................ selectedIds.add(id); selectedConnections.add({ "TribeID": tribeId, "ConnectedUserID": connectedUserId, }); } } //.......................................................................................... //.......................................................................................... } // Enum to track media type................................................................ enum MediaType { none, image, video } //..........................................................................................