import 'dart:async'; import 'package:file_picker/file_picker.dart'; import 'package:onufitness/services/logger_service.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_thumbnail_video/video_thumbnail.dart'; import 'package:intl/intl.dart'; import 'package:onufitness/constants/api_endpoints.dart'; import 'package:onufitness/models/master_dropdowns/fitness_goals_dropdown_response_model.dart'; import 'package:onufitness/screens/u_vault/models/failed_videos_draft_model.dart'; import 'package:onufitness/screens/u_vault/models/uvault_videos_response_model.dart'; import 'package:onufitness/services/api_services/base_api_services.dart'; import 'package:onufitness/services/local_storage_services/shared_services.dart'; import 'package:onufitness/utils/custom_sneakbar.dart'; class UvaultController extends GetxController { final logger = LoggerService(); @override void onInit() { super.onInit(); callOnInitMethods(); } callOnInitMethods() async { loadDraftVideos(); await fetchFitnessGoals(); await fetchVideos(isMyVideoScreen: false); } // Search functionality Timer? searchDebounceTimer; //........................................................................................................... final RxnString currentPlayingVideoUrl = RxnString(); // Video state management final RxMap videoInitializingStates = {}.obs; final RxMap videoInitializedStates = {}.obs; final RxMap videoPlayingStates = {}.obs; void setCurrentPlayingVideo(String videoUrl) { // Clear states for all other videos for (String url in videoInitializedStates.keys.toList()) { if (url != videoUrl) { videoInitializingStates[url] = false; videoInitializedStates[url] = false; videoPlayingStates[url] = false; } } currentPlayingVideoUrl.value = videoUrl; videoInitializingStates[videoUrl] = true; } //...................................................................................................... //........... Upload videos...................................................................... Rx selectedFile = Rx(null); var selectedCategory = ''.obs; var files = [].obs; TextEditingController videoTitleController = TextEditingController(); void setCategory(String? value) { if (value != null) selectedCategory.value = value; } RxDouble uploadProgress = 0.0.obs; RxBool isUploading = false.obs; RxString uploadStatus = "".obs; //............................................................................................................ //.......... SetUp IsLoading for Draft Videos Retry Button...................................................... var uploadingIndexes = {}.obs; int draftVideoIndex = 0; void setUploading(bool value) { if (value) { uploadingIndexes.add(draftVideoIndex); } else { uploadingIndexes.remove(draftVideoIndex); } } // Upload API call................................................................................................. Future uploadFileToAPI() async { var ret = false; final filePath = selectedFile.value?.path; if (filePath == null) { customSnackbar(title: "Error", message: "No file selected"); return false; } isUploading(true); setUploading(true); uploadStatus("Uploading..."); uploadProgress(0.0); Timer? timer; timer = Timer.periodic(Duration(milliseconds: 100), (t) { if (uploadProgress.value < 0.9) { uploadProgress.value += 0.05; } else { t.cancel(); } }); try { final xfile = XFile(filePath); var response = await ApiBase.postAnyFileWithMimeType( extendedURL: ApiUrl.uploadUvaultVideos, file: xfile, fileNameKey: 'UVaultFile', body: { "UserID": SharedServices.getUserDetails()?.data?.userId.toString(), "FitnessGoalId": selectedFitnessGoalId.value, "Title": videoTitleController.text, }, ); timer.cancel(); if (response.statusCode == 200 || response.statusCode == 201) { uploadProgress(1.0); uploadStatus("Completed"); customSnackbar(title: "Success", message: "File uploaded successfully"); ret = true; // selectedFitnessGoal.value = ""; // selectedFitnessGoalId.value = 0; // videoTitleController.clear(); } else { uploadProgress(0.0); uploadStatus("Failed"); customSnackbar(title: "Error", message: "Failed to upload file"); final failedUpload = FailedUploadModel( userId: SharedServices.getUserDetails()?.data?.userId.toString() ?? '', fitnessGoalId: selectedFitnessGoalId.value, title: videoTitleController.text, videoFilePath: selectedFile.value?.path ?? '', failedDate: DateFormat('dd-MM-yyyy').format(DateTime.now()), ); await SharedServices.addFailedUpload(failedUpload); selectedFile.value = null; ret = false; } } catch (e) { uploadProgress(0.0); uploadStatus("Failed"); customSnackbar(title: "Error", message: "Failed to upload file"); final failedUpload = FailedUploadModel( userId: SharedServices.getUserDetails()?.data?.userId.toString() ?? '', fitnessGoalId: selectedFitnessGoalId.value, title: videoTitleController.text, videoFilePath: selectedFile.value?.path ?? '', failedDate: DateFormat('dd-MM-yyyy').format(DateTime.now()), ); await SharedServices.addFailedUpload(failedUpload); selectedFile.value = null; ret = false; } finally { setUploading(false); isUploading(false); } return ret; } void cancelUpload() { selectedFile.value = null; uploadProgress(0); isUploading(false); } //............. Load Draft Videos From Shared Preferences to This GetX Controller........................... RxList draftVideos = [].obs; void loadDraftVideos() { draftVideos.value = SharedServices.getFailedUploads(); } void deleteDraftVideo(int index) { SharedServices.removeFailedUploadAt(index); loadDraftVideos(); } //............... See More Feature in the Draft Video list........................................... RxList seeMoreList = RxList(); void initializeSeeMoreList(int itemCount) { seeMoreList.value = List.filled(itemCount, false); } void toggleSeeMore(int index) { seeMoreList[index] = !seeMoreList[index]; update(); } //..................................................................................................................... //............... Fetch Fitness Goals List................................................................ var isFitnessGoalsLoading = false.obs; var apiFitnessGoalsList = [].obs; var selectedFitnessGoal = "".obs; RxList localFitnessGoalsList = [].obs; RxInt selectedFitnessGoalId = 0.obs; Future fetchFitnessGoals() async { isFitnessGoalsLoading(true); try { var response = await ApiBase.getRequest( extendedURL: ApiUrl.fetchFitnessGoals, sendHeaders: false, ); if (response.statusCode == 200 || response.statusCode == 201) { var responseData = fitnessGoalsResponseModelFromJson(response.body); if (responseData.isSuccess == true) { apiFitnessGoalsList.assignAll(responseData.data ?? []); localFitnessGoalsList.clear(); for (var goal in responseData.data!) { localFitnessGoalsList.add(goal.fitnessGoalTitle.toString()); } } else { var responseData = fitnessGoalsResponseModelFromJson(response.body); customSnackbar( title: "Error", message: responseData.message ?? "Failed to load Fitness Goals", ); } } else { var responseData = fitnessGoalsResponseModelFromJson(response.body); customSnackbar( title: "Error", message: responseData.message ?? "Failed to load Fitness Goals", ); } } catch (e) { logger.error("Exception in fetchFitnessGoals: $e"); customSnackbar(title: "Error", message: "Failed to load Fitness Goals"); } finally { isFitnessGoalsLoading(false); } } //... View Videos...................................................................................................... //fetchVideos API Call................................................................................................ RxList videos = [].obs; RxList myVideos = [].obs; RxBool isLoading = false.obs; final int pageSize = 5; //..... int currentPageUvault = 1; RxBool isLastPageUvault = false.obs; int currentPageMyVideos = 1; RxBool isLastPageMyVideos = false.obs; //..... void resetPagination({required bool isMyVideoScreen, int? newFitnessGoalId}) { if (isMyVideoScreen) { currentPageMyVideos = 1; isLastPageMyVideos.value = false; myVideos.clear(); } else { currentPageUvault = 1; isLastPageUvault.value = false; videos.clear(); } if (newFitnessGoalId != null) { selectedFitnessGoalId.value = newFitnessGoalId; } } RxString searchQueryText = "".obs; Future fetchVideos({ bool isRefresh = false, String? userId, String? fitnessGoalId, required bool isMyVideoScreen, String? searchQuery, }) async { if (isLoading.value) { return; } if (isMyVideoScreen) { if (isRefresh) { currentPageMyVideos = 1; isLastPageMyVideos.value = false; myVideos.clear(); } } if (!isMyVideoScreen) { if (isRefresh) { currentPageUvault = 1; isLastPageUvault.value = false; videos.clear(); } } isLoading.value = true; try { // Always required Map queryParams = { 'PageNumber': isMyVideoScreen ? currentPageMyVideos.toString() : currentPageUvault.toString(), 'PageSize': pageSize.toString(), }; // Add optional params only if they are not null or empty if (userId?.isNotEmpty == true) queryParams['UserId'] = userId!; if (fitnessGoalId?.isNotEmpty == true) { queryParams['FitnessGoalId'] = fitnessGoalId!; } if (searchQuery?.isNotEmpty == true) { queryParams['Title'] = searchQuery!; } // Build query string final queryString = queryParams.entries .map((e) => "${e.key}=${e.value}") .join("&"); final response = await ApiBase.getRequest( extendedURL: "${ApiUrl.fetchUvaultVideos}?$queryString", ); if (response.body.trim().isEmpty) { isMyVideoScreen ? isLastPageMyVideos.value = true : isLastPageUvault.value = true; return; } final parsed = uvaultVideosResponseModelFromJson(response.body); if (parsed.isSuccess == true) { final newVideos = parsed.data?.items ?? []; if (isMyVideoScreen) { if (currentPageMyVideos == 1 && newVideos.isEmpty) { isLastPageMyVideos.value = true; } } if (!isMyVideoScreen) { if (currentPageUvault == 1 && newVideos.isEmpty) { isLastPageUvault.value = true; } } if (newVideos.isNotEmpty) { if (userId == null) { videos.addAll(newVideos); } else { myVideos.addAll(newVideos); } } if (newVideos.length < pageSize) { isMyVideoScreen ? isLastPageMyVideos.value = true : isLastPageUvault.value = true; } else { isMyVideoScreen ? currentPageMyVideos++ : currentPageUvault++; } } else { isMyVideoScreen ? isLastPageMyVideos.value = true : isLastPageUvault.value = true; } } catch (e) { logger.error("Exception during video fetch: $e"); isMyVideoScreen ? isLastPageMyVideos.value = true : isLastPageUvault.value = true; } finally { isLoading.value = false; } } //............... Delete U-Vault............................................................................ RxBool isDeleteLoading = false.obs; Future deleteUVault({required String uVaultId}) async { isDeleteLoading(true); try { final response = await ApiBase.deleteRequest( extendedURL: "${ApiUrl.deleteUvault}/$uVaultId", body: {}, ); if (response.statusCode == 200 || response.statusCode == 201) { customSnackbar( title: "Success", message: "UVault deleted successfully.", ); resetPagination(isMyVideoScreen: true); await fetchVideos( userId: SharedServices.getUserDetails()!.data!.userId!.toString(), isMyVideoScreen: true, isRefresh: true, ); } else { customSnackbar( title: "Error", message: "Failed to delete UVault. Please try again.", ); } } catch (e) { logger.error("Exception while deleting UVault: $e"); customSnackbar(title: "Error", message: "Something went wrong."); } isDeleteLoading(false); } //............... Edit U-Vault............................................................................ RxBool isUpdating = false.obs; RxString updateUvaultId = "".obs; Future updateUVault() async { var ret = false; isUpdating(true); // Add these lines to reset and show upload progress uploadStatus("Uploading..."); uploadProgress(0.0); Timer? timer; timer = Timer.periodic(Duration(milliseconds: 100), (t) { if (uploadProgress.value < 0.9) { uploadProgress.value += 0.05; } else { t.cancel(); } }); //......................................................... try { final Map body = { "UserID": SharedServices.getUserDetails()?.data?.userId.toString(), }; if (selectedFitnessGoalId.value != 0) { body["FitnessGoalId"] = selectedFitnessGoalId.value; } if (videoTitleController.text.isNotEmpty) { body["Title"] = videoTitleController.text; } final filePath = selectedFile.value?.path; XFile? xfile; if (filePath != null) { xfile = XFile(filePath); } final response = await ApiBase.patchAnyFileWithMimeType( extendedURL: "${ApiUrl.updateUvault}/$updateUvaultId", file: xfile, fileNameKey: 'UVaultFile', body: body, ); timer.cancel(); if (response.statusCode == 200 || response.statusCode == 201) { uploadProgress(1.0); uploadStatus("Completed"); Future.delayed(Duration(milliseconds: 300), () { customSnackbar( title: "Success", message: "Video updated successfully", duration: 2, ); }); selectedFitnessGoalId.value = 0; selectedFitnessGoal.value = ""; updateUvaultId.value = ""; videoTitleController.clear(); selectedFile.value = null; ret = true; } else { uploadProgress(0.0); uploadStatus("Failed"); customSnackbar(title: "Error", message: "Failed to update video"); } } catch (e) { logger.error("Exception updateUVault: $e"); uploadProgress(0.0); uploadStatus("Failed"); customSnackbar(title: "Error", message: "Something went wrong"); } finally { isUpdating(false); } return ret; } @override void dispose() { searchDebounceTimer?.cancel(); super.dispose(); } }