bookmywages/lib/view/user_main_screens/detail_service_page.dart
2025-10-16 11:21:52 +05:30

2244 lines
122 KiB
Dart

import 'package:bookmywages/consts_widgets/app_assets.dart';
import 'package:bookmywages/consts_widgets/app_colors.dart';
import 'package:bookmywages/routers/consts_router.dart';
import 'package:bookmywages/view/user_main_screens/image_page.dart';
import 'package:bookmywages/viewmodel/api_controller.dart';
import 'package:bookmywages/viewmodel/consts_api.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dotted_line/dotted_line.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get/get.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
import 'package:video_player/video_player.dart';
class DetailServicePage extends ConsumerStatefulWidget {
final int id;
const DetailServicePage({super.key, required this.id});
@override
ConsumerState<DetailServicePage> createState() => _DetailServicePageState();
}
class _DetailServicePageState extends ConsumerState<DetailServicePage>
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
int selectedButton = 1;
final ScrollController _scrollController = ScrollController();
final ScrollController localScrollController = ScrollController();
int _currentServiceId = 0;
int selectedIndex = 1;
// Store current detail for button access
dynamic currentDetail;
// Video related variables
final List<VideoPlayerController> _controllers = [];
final List<YoutubePlayerController> _youtubeControllers = [];
bool _isDisposed = false;
bool _controllersInitialized = false;
// Form related variables
final _formKey = GlobalKey<FormState>();
AutovalidateMode _autoValidateMode = AutovalidateMode.disabled;
final nameController = TextEditingController();
final phoneController = TextEditingController();
final emailController = TextEditingController();
final messageController = TextEditingController();
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_currentServiceId = widget.id;
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
_isDisposed = true;
WidgetsBinding.instance.removeObserver(this);
// Dispose controllers safely
_disposeControllers();
// Dispose scroll controllers
_scrollController.dispose();
localScrollController.dispose();
// Dispose text controllers
nameController.dispose();
phoneController.dispose();
emailController.dispose();
messageController.dispose();
super.dispose();
}
// Required method for AutomaticKeepAliveClientMixin
@override
void didChangeMetrics() {
// Handle device metrics changes (screen rotation, keyboard, etc.)
if (mounted) {
debugPrint('Device metrics changed');
}
}
// Handle app lifecycle changes
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.paused:
_pauseAllVideos();
break;
case AppLifecycleState.resumed:
break;
case AppLifecycleState.inactive:
break;
case AppLifecycleState.detached:
break;
case AppLifecycleState.hidden:
break;
}
}
void _pauseAllVideos() {
try {
for (var controller in _controllers) {
if (controller.value.isInitialized && controller.value.isPlaying) {
controller.pause();
}
}
} catch (e) {
debugPrint('Error pausing videos: $e');
}
}
void _disposeControllers() {
// Dispose video controllers
for (var controller in _controllers) {
try {
if (!controller.value.isInitialized) continue;
controller.pause();
controller.dispose();
} catch (e) {
debugPrint('Error disposing video controller: $e');
}
}
_controllers.clear();
// Dispose YouTube controllers
for (var controller in _youtubeControllers) {
try {
controller.dispose();
} catch (e) {
debugPrint('Error disposing YouTube controller: $e');
}
}
_youtubeControllers.clear();
_controllersInitialized = false;
}
// Method to check if URL is YouTube link
bool isYoutubeLink(String url) {
return url.contains('youtube.com') ||
url.contains('youtu.be') ||
url.contains('m.youtube.com');
}
// Method to check if URL is direct video
bool isDirectVideo(String url) {
return url.contains('.mp4') ||
url.contains('.mov') ||
url.contains('.avi') ||
url.contains('.mkv') ||
url.contains('.webm') ||
url.contains('.3gp') ||
url.contains('.flv');
}
// Initialize video controllers with better error handling
void _initializeVideoControllers(List<String> videoUrls) {
if (_isDisposed || _controllersInitialized) return;
// Dispose existing controllers first
_disposeControllers();
try {
for (String url in videoUrls) {
if (_isDisposed) break;
if (isDirectVideo(url)) {
final controller = VideoPlayerController.network(url);
controller
.initialize()
.then((_) {
if (!_isDisposed && mounted) {
setState(() {});
}
})
.catchError((error) {
debugPrint('Error initializing video controller: $error');
});
_controllers.add(controller);
} else if (isYoutubeLink(url)) {
final videoId = YoutubePlayer.convertUrlToId(url);
if (videoId != null && videoId.isNotEmpty) {
try {
final youtubeController = YoutubePlayerController(
initialVideoId: videoId,
flags: const YoutubePlayerFlags(
autoPlay: false,
mute: false,
controlsVisibleAtStart: true,
),
);
_youtubeControllers.add(youtubeController);
} catch (e) {
debugPrint('Error creating YouTube controller: $e');
}
}
}
}
_controllersInitialized = true;
} catch (e) {
debugPrint('Error in _initializeVideoControllers: $e');
}
}
// Method to update service and smooth scroll to top
void _updateService(dynamic item) {
if (_isDisposed) return;
setState(() {
_currentServiceId = item.id ?? widget.id;
});
// Dispose old controllers before loading new service
_disposeControllers();
// Smooth scroll to top to show the new service details
if (_scrollController.hasClients) {
_scrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 800),
curve: Curves.easeInOut,
);
}
}
void _onFieldChanged() {
if (_autoValidateMode == AutovalidateMode.onUserInteraction && mounted) {
setState(() {}); // Triggers error revalidation and hiding
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
backgroundColor: AppColors.secondprimary,
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 5.0,
),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () => Navigator.of(context).pop(),
),
const SizedBox(width: 8),
const Text(
"Service Details",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
),
],
),
),
Expanded(
child: Consumer(
builder: (context, ref, _) {
final detailAsyncValue = ref.watch(
detailpageProvider(_currentServiceId.toString()),
);
return detailAsyncValue.when(
data: (detailList) {
if (detailList.isEmpty) {
return const Center(child: Text("No details found."));
}
final detail = detailList.first;
// Store current detail for button access
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_isDisposed) {
currentDetail = detail;
}
});
final imageUrls = detail.images1 ?? [];
final videoUrls = detail.videos ?? [];
final categoryId = detail.category.toString() ?? '0';
// Initialize video controllers only once per service
if (!_controllersInitialized && videoUrls.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_isDisposed) {
_initializeVideoControllers(videoUrls);
}
});
}
final mostPopularAsyncValue = ref.watch(
mostPopularProvider(categoryId),
);
return SingleChildScrollView(
controller: _scrollController,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Animated container for smooth transition
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
child: Stack(
key: ValueKey(_currentServiceId),
clipBehavior: Clip.none,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(30),
child: imageUrls.isNotEmpty
? CachedNetworkImage(
imageUrl: imageUrls[0],
width: double.infinity,
height: 250,
fit: BoxFit.cover,
placeholder: (context, url) =>
Container(
width: double.infinity,
height: 250,
color: Colors.grey[300],
child: const Center(
child: Icon(
Icons.image,
size: 50,
color: Colors.grey,
),
),
),
errorWidget:
(
context,
url,
error,
) => Container(
width: double.infinity,
height: 250,
color: Colors.grey[300],
child: const Icon(
Icons.image_not_supported,
size: 50,
color: Colors.grey,
),
),
)
: Container(
width: double.infinity,
height: 250,
color: Colors.grey[300],
child: const Icon(
Icons.image_not_supported,
size: 50,
color: Colors.grey,
),
),
),
),
Positioned(
bottom: -100,
left: 25,
right: 25,
child: AnimatedContainer(
duration: const Duration(
milliseconds: 300,
),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFCFCFC),
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: const Color(0xFFC9C9C9),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(
0.1,
),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
detail.vendorName ?? '',
style: const TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20,
letterSpacing: 0.2,
color: Colors.black,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 5),
Text(
detail.servicename ?? '',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w600,
fontSize: 16,
letterSpacing: 0.18,
color: Color(0xFF5A5A5A),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 15),
const DottedLine(
dashColor: Color(0xFFBABABA),
lineThickness: 1,
),
Padding(
padding: const EdgeInsets.only(
top: 16.0,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Flexible(
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
const Text(
"Payment:",
style: TextStyle(
fontFamily:
'Gilroy-Bold',
fontWeight:
FontWeight.w700,
fontSize: 15.61,
),
),
const SizedBox(
height: 15,
),
Text(
'Rs ${detail.amount ?? 0}',
style: const TextStyle(
fontFamily:
'Gilroy-Medium',
fontWeight:
FontWeight.w500,
fontSize: 14,
color: Color(
0xFF636363,
),
),
),
],
),
),
Flexible(
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
const Text(
"Duration:",
style: TextStyle(
fontFamily:
'Gilroy-Bold',
fontWeight:
FontWeight.w700,
fontSize: 15.61,
),
),
const SizedBox(
height: 15,
),
Text(
detail.workingduration ??
'',
style: const TextStyle(
fontFamily:
'Gilroy-Medium',
fontWeight:
FontWeight.w500,
fontSize: 14,
color: Color(
0xFF636363,
),
),
),
],
),
),
Flexible(
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
const Text(
"Rating:",
style: TextStyle(
fontFamily:
'Gilroy-Bold',
fontWeight:
FontWeight.w700,
fontSize: 15.61,
),
),
const SizedBox(
height: 15,
),
Row(
children: [
const Icon(
Icons.star,
size: 16,
color: Colors.amber,
),
const SizedBox(
width: 4,
),
Text(
(((double.tryParse(
detail.averageReview ??
'0',
) ??
0.0) *
2)
.floor() /
2)
.toStringAsFixed(
1,
),
style: const TextStyle(
fontFamily:
'Gilroy-Medium',
fontWeight:
FontWeight
.w500,
fontSize: 14,
color: Color(
0xFF636363,
),
),
),
],
),
],
),
),
],
),
),
],
),
),
),
],
),
),
const SizedBox(height: 120),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: Container(
width: double.infinity,
height: 93,
decoration: BoxDecoration(
color: const Color(0xFFFAFCFF),
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: const Color(0xFFF1F1F1),
width: 1,
),
),
child: Row(
children: [
const SizedBox(width: 12),
ClipOval(
child: CachedNetworkImage(
imageUrl:
detail.profilePic?.toString() ?? '',
placeholder: (context, url) =>
Container(
width: 67,
height: 67,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey,
),
child: const Icon(
Icons.person,
color: Colors.white,
),
),
errorWidget: (context, url, error) =>
Container(
width: 67,
height: 67,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey,
),
child: const Icon(
Icons.person,
color: Colors.white,
),
),
width: 55,
height: 55,
fit: BoxFit.cover,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
detail.vendorname ?? '',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 20,
color: Color(0xFF353434),
),
),
const SizedBox(height: 4),
Text(
detail.categoryName ?? '',
style: const TextStyle(
fontFamily: 'Gilroy-Regular',
fontWeight: FontWeight.w400,
fontSize: 15,
color: Color(0xFF717171),
),
),
],
),
),
Row(
children: [
const Icon(
Icons.star,
color: Colors.orange,
size: 18,
),
const SizedBox(width: 4),
Text(
(((double.tryParse(
detail.averageReview ??
'0',
) ??
0.0) *
2)
.floor() /
2)
.toStringAsFixed(1),
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: 14,
color: Color(0xFF636363),
),
),
const SizedBox(width: 12),
],
),
],
),
),
),
const SizedBox(height: 10),
const SizedBox(height: 10),
// Two Buttons (Description / Review Switch)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: Container(
width: double.infinity,
height: 75,
decoration: BoxDecoration(
color: const Color(0xFFFAFCFF),
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: const Color(0xFFF1F1F1),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Description Button
GestureDetector(
onTap: () {
if (mounted) {
setState(() {
selectedButton = 1;
});
}
},
child: Container(
width: 130,
height: 48,
decoration: BoxDecoration(
color: selectedButton == 1
? AppColors.primary
: Colors.white,
borderRadius: BorderRadius.circular(
10,
),
border: Border.all(
color: selectedButton == 1
? AppColors.primary
: const Color(0xFFCFCFCF),
width: 1,
),
),
child: Center(
child: Text(
'Description',
style: TextStyle(
color: selectedButton == 1
? Colors.white
: Colors.black,
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w400,
fontSize: 15,
height: 13.17 / 18,
letterSpacing: 0.01 * 18,
),
),
),
),
),
const SizedBox(width: 16),
// Review Button
GestureDetector(
onTap: () {
if (mounted) {
setState(() {
selectedButton = 2;
});
ref.refresh(
getreviewuserProvider(
detail.id.toString(),
),
);
}
},
child: Container(
width: 130,
height: 48,
decoration: BoxDecoration(
color: selectedButton == 2
? AppColors.primary
: Colors.white,
borderRadius: BorderRadius.circular(
10,
),
border: Border.all(
color: selectedButton == 2
? AppColors.primary
: const Color(0xFFCFCFCF),
width: 1,
),
),
child: Center(
child: Text(
'Review',
style: TextStyle(
color: selectedButton == 2
? Colors.white
: Colors.black,
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w400,
fontSize: 15,
height: 13.17 / 18,
letterSpacing: 0.01 * 18,
),
),
),
),
),
],
),
),
),
),
const SizedBox(height: 20),
// Switch between Description and Review
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
),
child: selectedButton == 1
? Row(
children: [
Expanded(
child: Text.rich(
TextSpan(
children: [
const TextSpan(
text: 'Description: ',
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w600,
fontSize: 16,
height: 28 / 16,
letterSpacing: 0,
),
),
TextSpan(
text:
detail.description ?? '',
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w400,
fontSize: 16,
height: 28 / 16,
letterSpacing: 0,
),
),
],
),
),
),
],
)
: Consumer(
builder: (context, ref, _) {
final reviewsAsync = ref.watch(
getreviewuserProvider(
detail.id.toString(),
),
);
return reviewsAsync.when(
data: (reviews) {
if (reviews.isEmpty) {
return Center(
child: Container(
padding: const EdgeInsets.all(
16,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(
22,
),
border: Border.all(
color:
Colors.grey.shade300,
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black
.withOpacity(0.25),
offset: const Offset(
0,
1,
),
blurRadius: 4.3,
),
],
),
child: const Text(
'No reviews available for this service yet.',
style: TextStyle(
fontFamily:
'Gilroy-Medium',
fontSize: 16,
),
),
),
);
}
return Container(
width: double.infinity,
height: 150,
margin: const EdgeInsets.only(
bottom: 16,
),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(22),
border: Border.all(
color: Colors.grey.shade300,
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black
.withOpacity(0.25),
offset: const Offset(0, 1),
blurRadius: 4.3,
),
],
),
child: Scrollbar(
controller:
localScrollController,
thumbVisibility: true,
thickness: 6.0,
radius: const Radius.circular(
10,
),
child: SingleChildScrollView(
controller:
localScrollController,
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: reviews.map((
review,
) {
return Padding(
padding:
const EdgeInsets.only(
bottom: 8.0,
),
child: Row(
children: [
ClipOval(
child: CachedNetworkImage(
imageUrl:
review
.profilePic1
?.toString() ??
'',
placeholder: (context, url) => Container(
width: 39,
height: 39,
decoration: const BoxDecoration(
shape: BoxShape
.circle,
color: Colors
.grey,
),
child: const Icon(
Icons
.person,
size: 20,
color: Colors
.white,
),
),
errorWidget:
(
context,
url,
error,
) => Container(
width: 39,
height:
39,
decoration: const BoxDecoration(
shape: BoxShape
.circle,
color: Colors
.grey,
),
child: const Icon(
Icons
.person,
size:
20,
color: Colors
.white,
),
),
width: 39,
height: 39,
fit: BoxFit
.cover,
),
),
const SizedBox(
width: 8,
),
Expanded(
child: Text(
review.userName ??
'',
style: const TextStyle(
fontFamily:
'Gilroy-Bold',
fontWeight:
FontWeight
.w700,
fontSize: 20,
height:
13.17 /
20,
letterSpacing:
0.2,
),
),
),
Row(
children: List.generate(
int.tryParse(
review.review ??
'0',
) ??
0,
(
index,
) => const Icon(
Icons.star,
color: Colors
.amber,
size: 20,
),
),
),
],
),
);
}).toList(),
),
),
),
);
},
loading: () => SizedBox(
height: 150,
child: const Center(
child: Text('Loading reviews...'),
),
),
error: (error, stack) => SizedBox(
height: 150,
child: Center(
child: Text(
'Unable to load reviews',
style: const TextStyle(
color: Colors.red,
),
),
),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FloatingActionButton(
onPressed: () {
// TODO: Add phone call logic
},
backgroundColor: Colors.green, // Optional
shape:
const CircleBorder(), // Ensures it's a perfect circle
child: const Icon(
Icons.call,
color: Colors.white,
),
),
Image.asset(AppAssets.map),
Image.asset(AppAssets.share),
],
),
),
// Images Section
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text(
"Image :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20,
height: 1.0,
letterSpacing: 1.0,
),
),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ImagePage(mediaUrls: imageUrls),
),
);
},
child: const Text(
"View all",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 17,
height: 1.0,
letterSpacing: 1.0,
),
),
),
],
),
),
// Images ListView
SizedBox(
height: 120,
child: imageUrls.isEmpty
? const Center(
child: Text("No images available"),
)
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: imageUrls.length,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
itemBuilder: (context, index) {
return Container(
width: 120,
margin: const EdgeInsets.only(
right: 10,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
15,
),
border: Border.all(
color: Colors.grey.shade300,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
15,
),
child: CachedNetworkImage(
imageUrl: imageUrls[index],
fit: BoxFit.cover,
placeholder: (context, url) =>
Container(
color: Colors.grey[200],
child: const Center(
child: Icon(
Icons.image,
color: Colors.grey,
size: 30,
),
),
),
errorWidget:
(context, url, error) =>
Container(
color: Colors.grey[200],
child: const Center(
child: Icon(
Icons.error,
color: Colors.red,
size: 30,
),
),
),
),
),
);
},
),
),
// Videos Section
if (videoUrls.isNotEmpty) ...[
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
"Videos :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20,
height: 1.0,
letterSpacing: 1.0,
),
),
),
SizedBox(
height: 150,
child: ListView.builder(
key: ValueKey(
'video-list-$_currentServiceId',
),
scrollDirection: Axis.horizontal,
itemCount: videoUrls.length,
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
itemBuilder: (context, index) {
final url = videoUrls[index];
if (isYoutubeLink(url)) {
final videoId =
YoutubePlayer.convertUrlToId(url) ??
'';
if (videoId.isEmpty) {
return Container(
width: 113.14,
height: 101.83,
margin: const EdgeInsets.only(
right: 10,
),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
14.71,
),
border: Border.all(
color: Colors.grey.shade300,
),
),
child: const Text(
"Invalid YouTube URL",
),
);
}
// Use pre-created controller if available
YoutubePlayerController?
youtubeController;
if (index < _youtubeControllers.length) {
youtubeController =
_youtubeControllers[index];
} else {
youtubeController =
YoutubePlayerController(
initialVideoId: videoId,
flags: const YoutubePlayerFlags(
autoPlay: false,
mute: false,
),
);
}
return Container(
width: 113.14,
height: 101.83,
margin: const EdgeInsets.only(
right: 10,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
14.71,
),
border: Border.all(
color: Colors.grey.shade300,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
14.71,
),
child: YoutubePlayer(
controller: youtubeController,
showVideoProgressIndicator: true,
),
),
);
} else if (isDirectVideo(url)) {
// Find the matching controller for this URL
VideoPlayerController? controller;
if (index < _controllers.length) {
controller = _controllers[index];
}
return GestureDetector(
onTap: () {
if (controller != null &&
controller.value.isInitialized) {
if (mounted) {
setState(() {
if (controller!
.value
.isPlaying) {
controller.pause();
} else {
for (var c in _controllers) {
if (c != controller &&
c.value.isPlaying) {
c.pause();
}
}
controller.play();
}
});
}
}
},
child: Container(
width: 113.14,
height: 101.83,
margin: const EdgeInsets.only(
right: 10,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
14.71,
),
border: Border.all(
color: Colors.grey.shade300,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
14.71,
),
child: Stack(
alignment: Alignment.center,
children: [
if (controller != null &&
controller
.value
.isInitialized)
AspectRatio(
aspectRatio: controller
.value
.aspectRatio,
child: VideoPlayer(
controller,
),
)
else
Container(
color: Colors.grey[300],
child: const Center(
child: Text('Loading...'),
),
),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.black
.withOpacity(0.5),
shape: BoxShape.circle,
),
child: Icon(
controller != null &&
controller
.value
.isPlaying
? Icons.pause
: Icons.play_arrow,
color: Colors.white,
),
),
],
),
),
),
);
} else {
return Container(
width: 113.14,
height: 101.83,
margin: const EdgeInsets.only(
right: 10,
),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
14.71,
),
border: Border.all(
color: Colors.grey.shade300,
),
),
child: const Text("Invalid video"),
);
}
},
),
),
],
// Relevant Services Section
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
"Relevant Service :",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20,
height: 1.0,
letterSpacing: 1.0,
),
),
),
const SizedBox(height: 20),
// Dynamic most popular services from API
SizedBox(
height: 210,
child: mostPopularAsyncValue.when(
data: (services) {
// Filter out the current service from relevant services
final filteredServices = services
.where(
(service) =>
service.id != _currentServiceId,
)
.toList();
return filteredServices.isEmpty
? const Center(
child: Text(
'No relevant services found',
),
)
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: filteredServices.length,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
itemBuilder: (context, index) {
final item =
filteredServices[index];
bool isSelected =
item.id == _currentServiceId;
return AnimatedContainer(
duration: const Duration(
milliseconds: 300,
),
width: 180,
margin: const EdgeInsets.only(
right: 16,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(20),
border: Border.all(
color: isSelected
? AppColors.primary
: AppColors.lightGrey,
width: isSelected ? 2 : 1,
),
boxShadow: isSelected
? [
BoxShadow(
color: AppColors
.primary
.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(
0,
4,
),
),
]
: [],
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
ClipRRect(
borderRadius:
const BorderRadius.vertical(
top: Radius.circular(
12,
),
),
child:
(item.images1 != null &&
item
.images1!
.isNotEmpty)
? CachedNetworkImage(
imageUrl: item
.images1!
.first,
width:
double.infinity,
height: 100,
fit: BoxFit.cover,
placeholder:
(
context,
url,
) => Container(
width: double
.infinity,
height: 100,
color: Colors
.grey[300],
child: const Icon(
Icons.image,
color: Colors
.grey,
),
),
errorWidget:
(
context,
url,
error,
) => Container(
width: double
.infinity,
height: 100,
color: Colors
.grey[300],
child: const Icon(
Icons
.image_not_supported,
color: Colors
.grey,
),
),
)
: Container(
width:
double.infinity,
height: 100,
color: Colors
.grey[300],
child: const Icon(
Icons
.image_not_supported,
color:
Colors.grey,
),
),
),
Align(
alignment:
Alignment.centerRight,
child: Container(
height: 30,
width: 80,
transform:
Matrix4.translationValues(
-10,
-15,
0,
),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius:
BorderRadius.circular(
20,
),
border: Border.all(
color: Colors.white,
width: 1.5,
),
),
child: Center(
child: Text(
'Rs.${item.amount ?? "N/A"}',
style: const TextStyle(
fontFamily:
'Gilroy-Bold',
fontWeight:
FontWeight.w400,
fontSize: 16.86,
height:
15.17 / 16.86,
letterSpacing: 0.02,
color: Color(
0xFFFCFAFA,
),
),
),
),
),
),
Padding(
padding:
const EdgeInsets.symmetric(
horizontal: 8,
),
child: Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Expanded(
child: Text(
item.serviceName ??
'Service',
style: const TextStyle(
fontFamily:
'Gilroy-Medium',
fontWeight:
FontWeight
.w700,
fontSize:
15.02,
height:
16.22 /
18.02,
letterSpacing:
0.0,
),
maxLines: 1,
overflow:
TextOverflow
.ellipsis,
),
),
Row(
children: [
const Icon(
Icons.star,
color: Colors
.orange,
size: 18,
),
const SizedBox(
width: 4,
),
Text(
(((double.tryParse(
item.averageReview?.toString() ??
'0',
) ??
0.0) *
2)
.floor() /
2)
.toStringAsFixed(
1,
),
textAlign:
TextAlign
.center,
style: const TextStyle(
fontFamily:
'SF UI Display',
fontWeight:
FontWeight
.w600,
fontSize:
14.17,
height:
20.09 /
14.17,
letterSpacing:
-0.5,
),
),
],
),
],
),
const SizedBox(
height: 10,
),
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Container(
width: 125,
padding:
const EdgeInsets.all(
4,
),
decoration: BoxDecoration(
border: Border.all(
color: AppColors
.lightGrey,
),
color: Colors
.white,
borderRadius:
BorderRadius.circular(
12,
),
),
child: Row(
children: [
item.profilePic1 !=
null
? CircleAvatar(
radius:
15,
backgroundImage: NetworkImage(
item.profilePic1!,
),
)
: const CircleAvatar(
radius:
15,
backgroundImage: AssetImage(
AppAssets.login,
),
),
const SizedBox(
width: 10,
),
Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
Text(
item.vendorName ??
'Vendor',
style: const TextStyle(
fontFamily:
'Gilroy-Medium',
fontWeight:
FontWeight.w400,
fontSize:
11.75,
height:
10.57 /
11.75,
letterSpacing:
0.0,
color: Color(
0xFF353434,
),
),
maxLines:
1,
overflow:
TextOverflow.ellipsis,
),
const SizedBox(
height:
5,
),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth:
75,
),
child: Text(
item.serviceName ??
'Service',
style: const TextStyle(
fontFamily:
'Gilroy-Regular',
fontWeight:
FontWeight.w400,
fontSize:
11.75,
height:
10.57 /
11.75,
letterSpacing:
0.0,
color: Color(
0xFF717171,
),
),
maxLines:
1,
overflow:
TextOverflow.ellipsis,
),
),
],
),
],
),
),
GestureDetector(
onTap: () =>
_updateService(
item,
),
child: AnimatedContainer(
duration:
const Duration(
milliseconds:
200,
),
padding:
const EdgeInsets.all(
8,
),
decoration: BoxDecoration(
color: AppColors
.primary,
borderRadius:
BorderRadius.circular(
12,
),
),
child: Icon(
isSelected
? Icons
.check_rounded
: Icons
.arrow_forward_ios_rounded,
size: 16,
color: Colors
.white,
),
),
),
],
),
],
),
),
],
),
);
},
);
},
loading: () => const SizedBox(
height: 210,
child: Center(
child: Text('Loading services...'),
),
),
error: (error, stack) => SizedBox(
height: 210,
child: Center(
child: Text(
'Error loading services',
style: TextStyle(color: Colors.red),
),
),
),
),
),
// Two Buttons Section
Center(
child: Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16,
top: 40,
),
child: Row(
children: [
Expanded(child: buildButton("Enquiry", 0)),
const SizedBox(width: 16),
Expanded(child: buildButton("Book now", 1)),
],
),
),
),
const SizedBox(height: 50), // Bottom padding
],
),
);
},
loading: () => const Center(
child: Padding(
padding: EdgeInsets.all(50.0),
child: Text('Loading service details...'),
),
),
error: (err, stack) => Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 48,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
"Failed to load service details",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
"Please check your connection and try again",
style: TextStyle(color: Colors.grey[600]),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
ref.refresh(
detailpageProvider(
_currentServiceId.toString(),
),
);
},
child: const Text('Retry'),
),
],
),
),
),
);
},
),
),
],
),
),
);
}
Widget buildButton(String text, int index) {
final bool isSelected = selectedIndex == index;
return Container(
height: 66,
decoration: BoxDecoration(
color: isSelected ? Color(0xFF0066FF) : Colors.white,
borderRadius: BorderRadius.circular(48),
border: Border.all(color: Color(0xFFCFCFCF)),
),
child: TextButton(
onPressed: () {
if (mounted) {
setState(() {
selectedIndex = index;
});
if (index == 0) {
// Show enquiry bottom sheet
_showEnquiryBottomSheet();
} else {
// Navigate to booking page with current detail
if (currentDetail != null) {
Get.toNamed(
RouterConts.bookingserivce,
arguments: currentDetail,
);
} else {
// Fallback: Get detail from provider
final detailAsyncValue = ref.read(
detailpageProvider(_currentServiceId.toString()),
);
detailAsyncValue.whenData((detailList) {
if (detailList.isNotEmpty && mounted) {
Get.toNamed(
RouterConts.bookingserivce,
arguments: detailList.first,
);
}
});
}
}
}
},
child: Text(
text,
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 18,
height: 21.5 / 23.89,
letterSpacing: 0.0,
color: isSelected ? Colors.white : const Color(0xFF292929),
),
),
),
);
}
void _showEnquiryBottomSheet() {
if (!mounted) return;
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(40)),
),
backgroundColor: Colors.white,
builder: (BuildContext context) {
return Padding(
padding: EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 24.0,
bottom: MediaQuery.of(context).viewInsets.bottom + 24.0,
),
child: SizedBox(
width: double.infinity,
height: 600, // Increased height for 5 fields
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Custom app bar-like header
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: const Icon(Icons.arrow_back_ios_new, size: 20),
),
const Expanded(
child: Center(
child: Text(
"Enquiry",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
// Invisible icon to balance the row for perfect center alignment
const Opacity(
opacity: 0,
child: Icon(Icons.arrow_back_ios_new, size: 20),
),
],
),
const SizedBox(height: 24),
Expanded(
child: SingleChildScrollView(
child: Form(
key: _formKey,
autovalidateMode: _autoValidateMode,
child: Column(
children: [
_buildField(
label: "Name",
controller: nameController,
validator: (value) =>
value == null || value.trim().isEmpty
? "Name is required"
: null,
),
const SizedBox(height: 16),
_buildField(
label: "Mobile Number",
controller: phoneController,
keyboardType: TextInputType.phone,
validator: (value) =>
value == null || value.length != 10
? "Enter valid mobile number"
: null,
),
const SizedBox(height: 16),
_buildField(
label: "Email",
controller: emailController,
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return "Email is required";
}
final regex = RegExp(r'^[^@]+@[^@]+\.[^@]+');
return regex.hasMatch(value)
? null
: "Enter a valid email";
},
),
const SizedBox(height: 16),
_buildField(
label: "Message",
controller: messageController,
maxLines: 4,
height: 120,
validator: (value) => value == null || value.isEmpty
? "Message is required"
: null,
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 55.38,
child: ElevatedButton(
onPressed: () async {
if (mounted) {
setState(() {
_autoValidateMode =
AutovalidateMode.onUserInteraction;
});
if (_formKey.currentState!.validate()) {
try {
final repo = ref.read(
enquriyupdateRepositoryProvider,
);
await repo.updateEnquriy(
context: context,
url: ConstsApi.enquery,
name: nameController.text.trim(),
number: phoneController.text.trim(),
email: emailController.text.trim(),
message: messageController.text.trim(),
serviceid: _currentServiceId.toString(),
);
// Clear form fields
nameController.clear();
phoneController.clear();
emailController.clear();
messageController.clear();
if (mounted) {
Navigator.pop(context);
}
} catch (e) {
if (mounted) {
Fluttertoast.showToast(
msg: 'Submission failed: $e',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
);
}
}
}
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.52),
),
),
child: const Text(
"Submit",
style: TextStyle(
fontFamily: 'Gilroy-Bold',
fontWeight: FontWeight.w700,
fontSize: 20.17,
height: 1.25,
letterSpacing: 1,
color: AppColors.secondprimary,
),
),
),
),
const SizedBox(height: 16), // Extra bottom padding
],
),
),
),
),
],
),
),
);
},
);
}
Widget _buildField({
required String label,
required TextEditingController controller,
required String? Function(String?) validator,
TextInputType keyboardType = TextInputType.text,
int maxLines = 1,
double height = 58,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 380,
height: height,
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFE1E1E1)),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.center,
child: TextFormField(
controller: controller,
keyboardType: keyboardType,
maxLines: maxLines,
decoration: InputDecoration(
hintText: label,
border: InputBorder.none,
),
validator: validator,
onChanged: (_) => _onFieldChanged(),
),
),
const SizedBox(height: 4),
],
);
}
}