2026-01-13 11:36:24 +05:30

1218 lines
42 KiB
Dart

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<String> selectedPrivacyConnectionIds = <String>[].obs;
final RxList<Map<String, dynamic>> selectedPrivacyConnections =
<Map<String, dynamic>>[].obs;
// Track original state for comparison
final RxList<String> originalPrivacyConnectionIds = <String>[].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<bool> 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<bool> updatePostPrivacy(int postId) async {
isUpdatingPrivacy.value = true;
try {
// Determine which connections were added and removed
final newConnections = <Map<String, dynamic>>[];
final deletedConnections = <Map<String, dynamic>>[];
// 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 = <SocialPostItem>[].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<String, SocialPostItem> postsCache =
<String, SocialPostItem>{}.obs;
// For video preview
Rxn<VideoPlayerController> videoController = Rxn<VideoPlayerController>();
final isVideoInitialized = false.obs;
final isVideoPlaying = false.obs;
PostModel postModel = PostModel();
RxBool isPostFetchedLoading = false.obs;
final isPaginationLoading = false.obs;
Future<bool> 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<void> 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<void> 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<bool> 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<PollSubmitResponseModel?> 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>(MediaType.none);
final _picker = ImagePicker();
Future<void> 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<void> 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<File>();
final selectedVideo = Rxn<File>();
TextEditingController captionCtrl = TextEditingController();
var captionText = ''.obs;
final visibility = 'Public'.obs;
final List<Map<String, dynamic>> 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<bool> 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<String, dynamic> 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<Map<String, dynamic>> selectedConnections =
<Map<String, dynamic>>[].obs;
final RxList<String> selectedIds = <String>[].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<bool> 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 }
//..........................................................................................