import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:onufitness/constants/api_enum_constant.dart'; import 'package:onufitness/constants/asset_constants.dart'; import 'package:onufitness/constants/color_constant.dart'; import 'package:onufitness/constants/text_constant.dart'; import 'package:onufitness/routes/route_constant.dart'; import 'package:onufitness/screens/u_vault/controllers/uvault_video_controller.dart'; import 'package:onufitness/screens/u_vault/widgets/filter_bottom_sheet.dart'; import 'package:onufitness/screens/u_vault/widgets/video_card.dart'; import 'package:onufitness/services/local_storage_services/shared_services.dart'; import 'package:onufitness/utils/helper_function.dart'; import 'package:onufitness/widgets/appbars/custom_appbar.dart'; import 'package:onufitness/widgets/Buttons/custom_submit_button.dart'; class UvaultViewScreen extends StatefulWidget { const UvaultViewScreen({super.key}); @override State createState() => _UvaultViewScreenState(); } class _UvaultViewScreenState extends State { final UvaultController controller = Get.put(UvaultController()); final ScrollController _scrollController = ScrollController(); final TextEditingController _searchController = TextEditingController(); final FocusNode _searchFocusNode = FocusNode(); @override void initState() { super.initState(); _scrollController.addListener(_scrollListener); } void _scrollListener() { if (!controller.isLastPageUvault.value && _scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { controller.fetchVideos( isMyVideoScreen: false, searchQuery: _searchController.text.trim(), ); } } void _performSearch({required String query}) { controller.resetPagination(isMyVideoScreen: false); controller.fetchVideos( isMyVideoScreen: false, searchQuery: query.trim(), isRefresh: true, ); } Future _handleRefresh() async { controller.resetPagination(isMyVideoScreen: false); await controller.fetchVideos( isMyVideoScreen: false, searchQuery: _searchController.text.trim(), isRefresh: true, ); } @override void dispose() { _scrollController.removeListener(_scrollListener); _scrollController.dispose(); _searchController.dispose(); _searchFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: CustomAppBar( backgroundColor: Colors.white, leading: IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.arrow_back_ios), ), actions: [ if (SharedServices.getLoginDetails()?.data?.userRole == ApiEnum.coachUserRole) TextButton( onPressed: () { Get.toNamed(RouteConstant.uploadUvaultVideos); }, child: Image.asset( AssetConstants.uploadTextWithIcon, height: isTablet ? 35.h : 25.h, ), ), IconButton( icon: Image.asset( AssetConstants.filterMenuIcon, height: isTablet ? 35.h : 25.h, ), onPressed: () { showFitnessGoalsFilterBottomSheet( context: context, isMyVideoScreen: false, ); }, ), ], title: "U-Vault", textColor: appbarTextColor, titleFontSize: appBarHeardingText, ), body: RefreshIndicator( onRefresh: _handleRefresh, color: Colors.black, child: Column( children: [ // Coach buttons SharedServices.getLoginDetails()?.data?.userRole == ApiEnum.coachUserRole ? Padding( padding: EdgeInsets.symmetric(horizontal: 15.w), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ CustomSubmitButton( width: 0.4.sw, height: 45.h, backgroundColor: Colors.white, textColor: Colors.black, textStyle: TextStyle( fontSize: 16.sp, color: Colors.black, ), borderSide: BorderSide(color: greyBorderColor), text: "My Videos", onPressed: () async { controller.resetPagination(isMyVideoScreen: true); Get.toNamed(RouteConstant.myUploadedUvaultVideos); await controller.fetchVideos( userId: SharedServices.getUserDetails()!.data!.userId! .toString(), isMyVideoScreen: true, ); }, ), CustomSubmitButton( width: 0.4.sw, height: 45.h, fontSize: 16.sp, backgroundColor: Colors.white, textStyle: TextStyle( fontSize: 16.sp, color: Colors.black, ), borderSide: BorderSide(color: greyBorderColor), text: "Draft Videos", onPressed: () { Get.toNamed(RouteConstant.draftUvaultVideos); }, ), ], ), ) : Container(), SizedBox(height: 10.h), // Search Bar Padding( padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.h), child: TextField( controller: _searchController, focusNode: _searchFocusNode, onChanged: (value) { controller.searchQueryText.value = value; // Debounce search - wait 500ms after user stops typing if (controller.searchDebounceTimer?.isActive ?? false) { controller.searchDebounceTimer?.cancel(); } controller.searchDebounceTimer = Timer( Duration(milliseconds: 500), () => _performSearch(query: value), ); }, decoration: InputDecoration( hintText: 'Search videos...', suffixIcon: Obx( () => controller.searchQueryText.value.isNotEmpty ? IconButton( icon: Icon(Icons.clear, color: Colors.grey), onPressed: () { controller.searchQueryText.value = ""; _searchController.clear(); _searchFocusNode.unfocus(); _performSearch(query: ''); }, ) : SizedBox.shrink(), ), hintStyle: TextStyle( fontWeight: FontWeight.w600, color: greyBorderColor, ), prefixIcon: const Padding( padding: EdgeInsets.only(left: 20), child: Icon(Icons.search), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(30.r), borderSide: BorderSide.none, ), filled: true, fillColor: textFieldFillColor, ), ), ), SizedBox(height: 10.h), // Video List Expanded( child: Obx(() { if (controller.videos.isEmpty && !controller.isLoading.value && controller.isLastPageUvault.value) { // Wrap empty state in CustomScrollView for pull-to-refresh return CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverFillRemaining( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.video_library_outlined, size: 60.sp, color: Colors.grey, ), SizedBox(height: 10.h), Text( _searchController.text.isEmpty ? "No videos available" : "No videos found", style: TextStyle( fontSize: 16.sp, color: Colors.grey, ), ), if (_searchController.text.isNotEmpty) ...[ SizedBox(height: 5.h), Text( 'Try a different search term', style: TextStyle( fontSize: 14.sp, color: Colors.grey[600], ), ), ], ], ), ), ), ], ); } if (controller.videos.isEmpty && controller.isLoading.value) { // Wrap loading state in CustomScrollView for pull-to-refresh return CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverFillRemaining( child: const Center( child: CircularProgressIndicator(color: Colors.black), ), ), ], ); } return ListView.builder( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), itemCount: controller.videos.length + (controller.isLastPageUvault.value ? 0 : 1), itemBuilder: (context, index) { if (index < controller.videos.length) { final video = controller.videos[index]; return Padding( padding: EdgeInsets.all(15.0.w), child: VideoCard( title: video.title ?? 'Untitled', category: video.fitnessGoalName?.toString() ?? 'Unknown', videoUrl: video.uVaultFile ?? '', videoId: video.uVaultId.toString(), isMyVideoScreen: false, thumbnailUrl: video.thumbnailFileName, ), ); } else { return const Padding( padding: EdgeInsets.all(16.0), child: Center( child: CircularProgressIndicator(color: Colors.black), ), ); } }, ); }), ), ], ), ), ); } }