1218 lines
42 KiB
Dart
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 }
|
|
|
|
//..........................................................................................
|