25-12-1/1
This commit is contained in:
@@ -37,7 +37,7 @@ class ContactUsPage extends StatelessWidget {
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
Get.put(TextToSpeechController()).speakText(
|
||||
'Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 8%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.'
|
||||
'Intaleq is the safest and most reliable ride-sharing app designed especially for passengers in Syria. We provide a comfortable, respectful, and affordable riding experience with features that prioritize your safety and convenience. Our trusted captains are verified, insured, and supported by regular car maintenance carried out by top engineers. We also offer on-road support services to make sure every trip is smooth and worry-free. With Intaleq, you enjoy quality, safety, and peace of mind—every time you ride.'
|
||||
.tr);
|
||||
},
|
||||
icon: const Icon(Icons.headphones),
|
||||
@@ -45,7 +45,7 @@ class ContactUsPage extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 8%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.'
|
||||
'Intaleq is the safest and most reliable ride-sharing app designed especially for passengers in Syria. We provide a comfortable, respectful, and affordable riding experience with features that prioritize your safety and convenience. Our trusted captains are verified, insured, and supported by regular car maintenance carried out by top engineers. We also offer on-road support services to make sure every trip is smooth and worry-free. With Intaleq, you enjoy quality, safety, and peace of mind—every time you ride.'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
@@ -62,7 +62,7 @@ class ContactUsPage extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
"You can contact us during working hours from 12:00 - 19:00."
|
||||
"You can contact us during working hours from 10:00 - 16:00."
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
|
||||
@@ -43,6 +43,7 @@ class MapPagePassenger extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
GoogleMapPassengerWidget(),
|
||||
// OsmMapPassengerWidget(),
|
||||
leftMainMenuIcons(),
|
||||
// PaymobPackage(),
|
||||
const PickerIconOnMap(),
|
||||
@@ -88,33 +89,33 @@ class CancelRidePageShow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
builder: (controller) =>
|
||||
(controller.data.isNotEmpty && controller.statusRide != 'Begin')
|
||||
// ||
|
||||
// controller.timeToPassengerFromDriverAfterApplied == 0
|
||||
? Positioned(
|
||||
right: box.read(BoxName.lang) != 'ar' ? 10 : null,
|
||||
left: box.read(BoxName.lang) == 'ar' ? 10 : null,
|
||||
top: Get.height * .013,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
controller.changeCancelRidePageShow();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.redColor,
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(3),
|
||||
child: Icon(
|
||||
Icons.clear,
|
||||
size: 40,
|
||||
color: AppColor.secondaryColor,
|
||||
),
|
||||
),
|
||||
builder: (controller) => (controller.polyLines.isNotEmpty &&
|
||||
controller.statusRide != 'Begin')
|
||||
// ||
|
||||
// controller.timeToPassengerFromDriverAfterApplied == 0
|
||||
? Positioned(
|
||||
right: box.read(BoxName.lang) != 'ar' ? 10 : null,
|
||||
left: box.read(BoxName.lang) == 'ar' ? 10 : null,
|
||||
top: Get.height * .013,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
controller.changeCancelRidePageShow();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.redColor,
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(3),
|
||||
child: Icon(
|
||||
Icons.clear,
|
||||
size: 40,
|
||||
color: AppColor.secondaryColor,
|
||||
),
|
||||
))
|
||||
: const SizedBox());
|
||||
),
|
||||
),
|
||||
))
|
||||
: const SizedBox());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,286 +1,412 @@
|
||||
import 'package:Intaleq/constant/colors.dart';
|
||||
import 'package:Intaleq/constant/links.dart';
|
||||
import 'package:Intaleq/constant/style.dart';
|
||||
import 'package:Intaleq/controller/firebase/firbase_messge.dart';
|
||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
||||
import 'package:Intaleq/main.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart'; // لتنسيق الأرقام
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../controller/firebase/notification_service.dart';
|
||||
import '../../../controller/functions/launch.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
|
||||
class ApplyOrderWidget extends StatelessWidget {
|
||||
ApplyOrderWidget({super.key});
|
||||
final firebaseMessagesController =
|
||||
Get.isRegistered<FirebaseMessagesController>()
|
||||
? Get.find<FirebaseMessagesController>()
|
||||
: Get.put(FirebaseMessagesController());
|
||||
const ApplyOrderWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color _parseColor(String colorHex) {
|
||||
// دالة لتحويل كود اللون الهيكس إلى لون
|
||||
Color parseColor(String colorHex) {
|
||||
if (colorHex.isEmpty) return Colors.grey;
|
||||
String processedHex = colorHex.replaceFirst('#', '0xff').trim();
|
||||
return Color(int.parse(processedHex.startsWith('0xff')
|
||||
? processedHex
|
||||
: '0xff$processedHex'));
|
||||
try {
|
||||
String processedHex = colorHex.replaceFirst('#', '').trim();
|
||||
if (processedHex.length == 6) processedHex = 'FF$processedHex';
|
||||
return Color(int.parse('0x$processedHex'));
|
||||
} catch (e) {
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
Get.put(
|
||||
FirebaseMessagesController()); // Ensure FirebaseMessagesController is initialized
|
||||
if (controller.statusRide == 'Apply' && !controller.isSearchingWindow) {
|
||||
return Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
// More modern BoxDecoration
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
boxShadow: [BoxShadow(blurRadius: 10, color: Colors.black12)],
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildPriceInfo(context, controller),
|
||||
const SizedBox(height: 16),
|
||||
_buildDriverInfoCard(context, controller, _parseColor),
|
||||
],
|
||||
),
|
||||
return Obx(() {
|
||||
final controller = Get.find<MapPassengerController>();
|
||||
|
||||
final bool isVisible =
|
||||
controller.currentRideState.value == RideState.driverApplied ||
|
||||
controller.currentRideState.value == RideState.driverArrived;
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.elasticOut, // تأثير حركي أجمل
|
||||
bottom: isVisible ? 0 : -Get.height * 0.6,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
// height: Get.height * 0.38, // زيادة الارتفاع قليلاً للتصميم الجديد
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 20,
|
||||
spreadRadius: 2,
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
offset: const Offset(0, -2),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
|
||||
child: GetBuilder<MapPassengerController>(
|
||||
builder: (c) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// مقبض صغير في الأعلى
|
||||
Container(
|
||||
width: 40,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// السعر والعنوان
|
||||
_buildPriceHeader(context, c),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// كرت المعلومات الرئيسي (سائق + سيارة)
|
||||
_buildMainInfoCard(context, c, parseColor),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// أزرار الاتصال
|
||||
_buildContactButtonsRow(context, c),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// شريط الوقت
|
||||
c.currentRideState.value == RideState.driverArrived
|
||||
? const DriverArrivePassengerAndWaitMinute()
|
||||
: const TimeDriverToPassenger(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPriceInfo(
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. قسم السعر (مع التنسيق الجديد)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildPriceHeader(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
String message;
|
||||
if (box.read(BoxName.carType) == 'Speed' ||
|
||||
box.read(BoxName.carType) == 'Awfar Car' ||
|
||||
box.read(BoxName.carType) == 'Delivery') {
|
||||
message =
|
||||
'This ride type does not allow changes to the destination or additional stops'
|
||||
.tr;
|
||||
} else {
|
||||
message =
|
||||
'This ride type allows changes, but the price may increase'.tr;
|
||||
}
|
||||
Get.snackbar(
|
||||
'This price is'.tr +
|
||||
' ${controller.totalPassenger.toStringAsFixed(2)}'.tr,
|
||||
message,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
duration: const Duration(seconds: 2),
|
||||
backgroundColor:
|
||||
AppColor.yellowColor.withOpacity(0.8), // More subtle background
|
||||
);
|
||||
},
|
||||
child: Center(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${'The driver accepted your order for'.tr} ',
|
||||
style: AppStyle.title),
|
||||
TextSpan(
|
||||
text: controller.totalPassenger.toStringAsFixed(2),
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.bold, color: AppColor.redColor),
|
||||
// تنسيق الرقم (مثلاً: 60,000)
|
||||
final formatter = NumberFormat("#,###");
|
||||
String formattedPrice = formatter.format(controller.totalPassenger);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'Driver Accepted Request'.tr,
|
||||
style: AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
formattedPrice,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
TextSpan(text: ' ${'LE'.tr}', style: AppStyle.title),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'SYP'.tr,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. كرت المعلومات الرئيسي (السائق + السيارة 3D)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildMainInfoCard(BuildContext context,
|
||||
MapPassengerController controller, Color Function(String) parseColor) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor, // لون خلفية فاتح
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// الجزء الأيسر: معلومات السائق
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// صورة السائق
|
||||
Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColor.primaryColor, width: 2),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 26,
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'),
|
||||
onBackgroundImageError: (exception, stackTrace) =>
|
||||
const Icon(Icons.person, size: 26, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// الاسم والتقييم والسيارة نص
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
controller.driverName,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 16, fontWeight: FontWeight.bold),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.star, color: Colors.amber, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
controller.driverRate,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${controller.model} • ${controller.licensePlate}',
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 12),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// الجزء الأيمن: أيقونة السيارة الـ 3D
|
||||
_build3DCarIcon(controller, parseColor),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. أيقونة السيارة الـ 3D (الدائرة والظلال والخلفية الذكية)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _build3DCarIcon(
|
||||
MapPassengerController controller, Color Function(String) parseColor) {
|
||||
Color carColor = parseColor(controller.colorHex);
|
||||
|
||||
// تحديد سطوع لون السيارة لتحديد لون الخلفية
|
||||
// إذا كانت السيارة فاتحة (أكثر من 0.6)، الخلفية تكون غامقة، والعكس
|
||||
bool isCarLight = carColor.computeLuminance() > 0.6;
|
||||
|
||||
// ألوان الخلفية للدائرة
|
||||
Color bgGradientStart =
|
||||
isCarLight ? Colors.blueGrey.shade700 : Colors.grey.shade100;
|
||||
Color bgGradientEnd =
|
||||
isCarLight ? Colors.blueGrey.shade900 : Colors.grey.shade300;
|
||||
Color borderColor = isCarLight ? Colors.blueGrey.shade600 : Colors.white;
|
||||
|
||||
return Container(
|
||||
width: 75,
|
||||
height: 75,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
// تدرج لوني للخلفية لتبدو 3D
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [bgGradientStart, bgGradientEnd],
|
||||
),
|
||||
border: Border.all(color: borderColor, width: 2),
|
||||
// ظلال لرفع الدائرة عن السطح
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(4, 4),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.white.withOpacity(isCarLight ? 0.1 : 0.8),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(-4, -4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(carColor, BlendMode.srcIn),
|
||||
child: Image.asset(
|
||||
box.read(BoxName.carType) == 'Scooter' ||
|
||||
box.read(BoxName.carType) == 'Pink Bike'
|
||||
? 'assets/images/moto.png'
|
||||
: 'assets/images/car3.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDriverInfoCard(BuildContext context,
|
||||
MapPassengerController controller, Color Function(String) parseColor) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).canvasColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4. أزرار الاتصال (بتصميم جديد)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildContactButtonsRow(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildActionButton(
|
||||
label: 'Message'.tr,
|
||||
icon: Icons.chat_bubble_outline_rounded,
|
||||
color: AppColor.blueColor,
|
||||
onTap: () => _showContactOptionsDialog(context, controller),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: _buildActionButton(
|
||||
label: 'Call'.tr,
|
||||
icon: Icons.phone_rounded,
|
||||
color: AppColor.greenColor,
|
||||
onTap: () {
|
||||
HapticFeedback.heavyImpact();
|
||||
makePhoneCall(controller.driverPhone);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
required String label,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return ElevatedButton(
|
||||
onPressed: onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: color.withOpacity(0.1),
|
||||
foregroundColor: color,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Column(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(box.read(BoxName.carType.toString()),
|
||||
style:
|
||||
AppStyle.title.copyWith(fontWeight: FontWeight.w500)),
|
||||
Row(
|
||||
children: [
|
||||
_buildCarDetails(context, controller),
|
||||
const SizedBox(width: 10),
|
||||
_buildCarImage(controller, parseColor),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1, thickness: 1, color: Colors.grey),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildDriverAvatarAndInfo(controller),
|
||||
_buildContactButtons(context, controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 12.0, right: 12.0, bottom: 12.0),
|
||||
child: controller.isDriverArrivePassenger
|
||||
? const DriverArrivePassengerAndWaitMinute()
|
||||
: const TimeDriverToPassenger(),
|
||||
Icon(icon, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCarDetails(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(controller.model.toString(), style: AppStyle.title),
|
||||
Text(controller.licensePlate.toString(),
|
||||
style: Theme.of(context).textTheme.bodyMedium),
|
||||
Text(controller.carColor.toString(),
|
||||
style: Theme.of(context).textTheme.bodyMedium),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCarImage(
|
||||
MapPassengerController controller, Color Function(String) parseColor) {
|
||||
return ColorFiltered(
|
||||
colorFilter:
|
||||
ColorFilter.mode(parseColor(controller.colorHex), BlendMode.srcIn),
|
||||
child: Image.asset(
|
||||
box.read(BoxName.carType) == 'Scooter' ||
|
||||
box.read(BoxName.carType) == 'Pink Bike'
|
||||
? 'assets/images/moto.png'
|
||||
: 'assets/images/car3.png',
|
||||
height: 60,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDriverAvatarAndInfo(MapPassengerController controller) {
|
||||
return Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'),
|
||||
onBackgroundImageError: (exception, stackTrace) =>
|
||||
const Icon(Icons.person, size: 30, color: AppColor.blueColor),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(controller.driverName,
|
||||
style: AppStyle.title.copyWith(fontWeight: FontWeight.w500)),
|
||||
Text('⭐ ${controller.driverRate}',
|
||||
style: const TextStyle(fontSize: 16, color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContactButtons(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => _showContactOptionsDialog(context, controller),
|
||||
icon: const Icon(Icons.message, color: AppColor.blueColor, size: 28),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.heavyImpact();
|
||||
makePhoneCall(controller.driverPhone);
|
||||
},
|
||||
icon: const Icon(Icons.call, color: AppColor.greenColor, size: 28),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// --- النوافذ المنبثقة للرسائل (نفس المنطق القديم) ---
|
||||
void _showContactOptionsDialog(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
Get.defaultDialog(
|
||||
title: 'Contact Options'.tr,
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
height: Get.height * .4,
|
||||
child: ListView(
|
||||
// shrinkWrap: true,
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Quick Message'.tr, style: AppStyle.title),
|
||||
const SizedBox(height: 15),
|
||||
..._buildPredefinedMessages(controller),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(height: 30),
|
||||
_buildCustomMessageInput(controller, context),
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).viewInsets.bottom), // للكيبورد
|
||||
],
|
||||
),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildPredefinedMessages(MapPassengerController controller) {
|
||||
const messages = [
|
||||
'Hello, I\'m at the agreed-upon location',
|
||||
'My location is correct. You can search for me using the navigation app',
|
||||
'I\'m waiting for you',
|
||||
"How much longer will you be?",
|
||||
];
|
||||
|
||||
return messages
|
||||
.map((message) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// firebaseMessagesController.sendNotificationToDriverMAP(
|
||||
// 'message From passenger',
|
||||
// message.tr,
|
||||
// controller.driverToken.toString(),
|
||||
// [],
|
||||
// 'ding',
|
||||
// );
|
||||
NotificationService.sendNotification(
|
||||
target: controller.driverToken.toString(),
|
||||
title: 'message From passenger',
|
||||
body: message.tr, // Make sure to translate the message
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'ding',
|
||||
driverList: [],
|
||||
);
|
||||
padding: const EdgeInsets.only(bottom: 10.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_sendMessage(controller, message.tr);
|
||||
Get.back();
|
||||
},
|
||||
child: Text(message.tr),
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12, horizontal: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.quickreply_rounded,
|
||||
size: 18, color: Colors.grey),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(message.tr, style: AppStyle.subtitle)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList();
|
||||
@@ -291,45 +417,59 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Form(
|
||||
key: controller.messagesFormKey,
|
||||
child: MyTextForm(
|
||||
controller: controller.messageToDriver,
|
||||
label: 'Send a custom message'.tr,
|
||||
hint: 'Type your message'.tr,
|
||||
type: TextInputType.text,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
child: Form(
|
||||
key: controller.messagesFormKey,
|
||||
child: TextFormField(
|
||||
controller: controller.messageToDriver,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type your message...'.tr,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (controller.messagesFormKey.currentState!.validate()) {
|
||||
// firebaseMessagesController.sendNotificationToDriverMAP(
|
||||
// 'message From passenger',
|
||||
// controller.messageToDriver.text,
|
||||
// controller.driverToken,
|
||||
// [],
|
||||
// 'ding',
|
||||
// );
|
||||
NotificationService.sendNotification(
|
||||
target: controller.driverToken.toString(),
|
||||
title: 'message From passenger',
|
||||
body: controller.messageToDriver.text,
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'ding',
|
||||
driverList: [],
|
||||
);
|
||||
controller.messageToDriver.clear();
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.send),
|
||||
const SizedBox(width: 10),
|
||||
CircleAvatar(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (controller.messagesFormKey.currentState!.validate()) {
|
||||
_sendMessage(controller, controller.messageToDriver.text);
|
||||
controller.messageToDriver.clear();
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.send_rounded, color: Colors.white, size: 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _sendMessage(MapPassengerController controller, String text) {
|
||||
NotificationService.sendNotification(
|
||||
category: 'message From passenger',
|
||||
target: controller.driverToken.toString(),
|
||||
title: 'Message From passenger'.tr,
|
||||
body: text,
|
||||
isTopic: false,
|
||||
tone: 'ding',
|
||||
driverList: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// مؤشرات الانتظار والوقت (نفس المنطق مع تحسين بسيط في التصميم)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class DriverArrivePassengerAndWaitMinute extends StatelessWidget {
|
||||
const DriverArrivePassengerAndWaitMinute({Key? key}) : super(key: key);
|
||||
|
||||
@@ -338,35 +478,31 @@ class DriverArrivePassengerAndWaitMinute extends StatelessWidget {
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Driver is waiting'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
controller.stringRemainingTimeDriverWaitPassenger5Minute,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: AppColor.redColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor: AppColor.accentColor.withOpacity(0.3),
|
||||
backgroundColor: Colors.grey[200],
|
||||
color: controller.remainingTimeDriverWaitPassenger5Minute < 60
|
||||
? AppColor.redColor
|
||||
: AppColor.greenColor,
|
||||
minHeight: 20,
|
||||
minHeight: 8,
|
||||
value:
|
||||
controller.progressTimerDriverWaitPassenger5Minute.toDouble(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Center(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${'Driver is waiting at pickup.'.tr} ',
|
||||
style: AppStyle.subtitle),
|
||||
TextSpan(
|
||||
text: controller
|
||||
.stringRemainingTimeDriverWaitPassenger5Minute,
|
||||
style: AppStyle.title),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
@@ -379,47 +515,37 @@ class TimeDriverToPassenger extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
return controller.isDriverInPassengerWay == false ||
|
||||
controller.timeToPassengerFromDriverAfterApplied > 0
|
||||
? Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor: AppColor.accentColor.withOpacity(0.3),
|
||||
color: controller
|
||||
.remainingTimeToPassengerFromDriverAfterApplied <
|
||||
60
|
||||
? AppColor.redColor
|
||||
: AppColor.greenColor,
|
||||
minHeight: 20,
|
||||
value: controller
|
||||
.progressTimerToPassengerFromDriverAfterApplied
|
||||
.toDouble()
|
||||
.clamp(0.0, 1.0),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Center(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${'Driver is on the way'.tr} ',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
TextSpan(
|
||||
text: controller.stringRemainingTimeToPassenger,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox();
|
||||
if (controller.timeToPassengerFromDriverAfterApplied <= 0) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Driver arriving in'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
controller.stringRemainingTimeToPassenger,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: AppColor.primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor: Colors.grey[200],
|
||||
color: AppColor.primaryColor,
|
||||
minHeight: 8,
|
||||
value: controller.progressTimerToPassengerFromDriverAfterApplied
|
||||
.toDouble()
|
||||
.clamp(0.0, 1.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,7 +515,7 @@ class Details extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text(
|
||||
'${'Distance is'.tr} ${controller.data[0]['distance']['text']}',
|
||||
'${'Distance is'.tr} ${controller.distance.toStringAsFixed(2)} KM',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:Intaleq/main.dart';
|
||||
import 'package:Intaleq/views/home/profile/passenger_profile_page.dart';
|
||||
import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
||||
import 'package:Intaleq/views/widgets/my_textField.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:ui';
|
||||
|
||||
import '../../../constant/info.dart';
|
||||
@@ -79,9 +80,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
_prepareCarTypes(controller);
|
||||
|
||||
if (!(controller.data.isNotEmpty &&
|
||||
controller.isBottomSheetShown &&
|
||||
controller.rideConfirm == false)) {
|
||||
if (!(controller.isBottomSheetShown) && controller.rideConfirm == false) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// Added a BackdropFilter for a modern glassmorphism effect
|
||||
@@ -351,7 +350,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
color: AppColor.primaryColor, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${controller.distance} ${'KM'.tr}',
|
||||
'${controller.distance.toStringAsFixed(1)} ${'KM'.tr}',
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: AppColor.primaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -418,30 +417,54 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
|
||||
// --- LOGIC METHODS (UNCHANGED) ---
|
||||
|
||||
// 1. قم بإضافة هذا السطر في أعلى الملف
|
||||
|
||||
String _getPassengerPriceText(
|
||||
CarType carType, MapPassengerController mapPassengerController) {
|
||||
// الخطوة 1: احصل على السعر كـ double أولاً
|
||||
double rawPrice;
|
||||
switch (carType.carType) {
|
||||
case 'Comfort':
|
||||
return mapPassengerController.totalPassengerComfort.toStringAsFixed(1);
|
||||
rawPrice = mapPassengerController.totalPassengerComfort;
|
||||
break;
|
||||
case 'Speed':
|
||||
return mapPassengerController.totalPassengerSpeed.toStringAsFixed(1);
|
||||
rawPrice = mapPassengerController.totalPassengerSpeed;
|
||||
break;
|
||||
case 'Electric':
|
||||
return mapPassengerController.totalPassengerElectric.toStringAsFixed(1);
|
||||
rawPrice = mapPassengerController.totalPassengerElectric;
|
||||
break;
|
||||
case 'Awfar Car':
|
||||
return mapPassengerController.totalPassengerBalash.toStringAsFixed(1);
|
||||
rawPrice = mapPassengerController.totalPassengerBalash;
|
||||
break;
|
||||
case 'Scooter':
|
||||
return mapPassengerController.totalPassengerScooter.toStringAsFixed(1);
|
||||
case 'Pink Bike': // دمج الحالات المتشابهة
|
||||
rawPrice = mapPassengerController.totalPassengerScooter;
|
||||
break;
|
||||
case 'Van':
|
||||
return mapPassengerController.totalPassengerVan.toStringAsFixed(1);
|
||||
rawPrice = mapPassengerController.totalPassengerVan;
|
||||
break;
|
||||
case 'Lady':
|
||||
return mapPassengerController.totalPassengerLady.toStringAsFixed(1);
|
||||
case 'Pink Bike':
|
||||
return mapPassengerController.totalPassengerScooter.toStringAsFixed(1);
|
||||
rawPrice = mapPassengerController.totalPassengerLady;
|
||||
break;
|
||||
case 'Rayeh Gai':
|
||||
return mapPassengerController.totalPassengerRayehGai.toStringAsFixed(1);
|
||||
rawPrice = mapPassengerController.totalPassengerRayehGai;
|
||||
break;
|
||||
default:
|
||||
return '...';
|
||||
return '...'; // إذا كان نوع السيارة غير معروف
|
||||
}
|
||||
|
||||
// الخطوة 2: قم بإزالة الكسور العشرية
|
||||
// .round() ستحول 65000.00 إلى 65000
|
||||
final int roundedPrice = rawPrice.round();
|
||||
|
||||
// الخطوة 3: أنشئ "مُنسّق" ليضيف فواصل الآلاف
|
||||
// NumberFormat.decimalPattern() يستخدم إعدادات اللغة الافتراضية للجهاز
|
||||
// لوضع الفاصلة (,) أو النقطة (.) حسب الدولة
|
||||
final formatter = NumberFormat.decimalPattern();
|
||||
|
||||
// الخطوة 4: قم بتنسيق الرقم الصحيح
|
||||
// سيحول 65000 إلى "65,000"
|
||||
return formatter.format(roundedPrice);
|
||||
}
|
||||
|
||||
void _showCarDetailsDialog(
|
||||
|
||||
@@ -137,9 +137,9 @@ class CashConfirmPageShown extends StatelessWidget {
|
||||
onPressed: () {
|
||||
// --- نفس منطقك القديم بالضبط ---
|
||||
controller.changeCashConfirmPageShown();
|
||||
controller.isSearchingWindow = true;
|
||||
controller.confirmRideForAllDriverAvailable();
|
||||
controller.update();
|
||||
// controller.isSearchingWindow = true;
|
||||
controller.startSearchingForDriver();
|
||||
// controller.update();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,8 +168,8 @@ class _SearchFieldState extends State<_SearchField> {
|
||||
icon: Icon(Icons.location_on_outlined,
|
||||
color: AppColor.accentColor, size: 30),
|
||||
tooltip: widget.controller.isAnotherOreder
|
||||
? 'Pick destination on map'
|
||||
: 'Pick on map',
|
||||
? 'Pick destination on map'.tr
|
||||
: 'Pick on map'.tr,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -7,8 +7,8 @@ import 'package:Intaleq/controller/home/points_for_rider_controller.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/functions/location_controller.dart';
|
||||
import '../../../controller/home/device_tier.dart';
|
||||
// import '../../../controller/functions/location_controller.dart'; // Un-comment if needed
|
||||
// import '../../../controller/home/device_tier.dart'; // Removed to rely on Controller logic
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
import '../../widgets/mydialoug.dart';
|
||||
@@ -17,7 +17,6 @@ class GoogleMapPassengerWidget extends StatelessWidget {
|
||||
GoogleMapPassengerWidget({super.key});
|
||||
|
||||
final WayPointController wayPointController = Get.put(WayPointController());
|
||||
final LocationController locationController = Get.find<LocationController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -32,24 +31,26 @@ class GoogleMapPassengerWidget extends StatelessWidget {
|
||||
child: GoogleMap(
|
||||
onMapCreated: controller.onMapCreated,
|
||||
|
||||
// ✅ حدود الكاميرا كما هي
|
||||
// ✅ Camera Bounds
|
||||
cameraTargetBounds: CameraTargetBounds(controller.boundsdata),
|
||||
|
||||
// ✅ Zoom أهدأ للأجهزة الضعيفة
|
||||
// ✅ Performance: Smoother zoom limits for low-end devices
|
||||
minMaxZoomPreference: controller.lowPerf
|
||||
? const MinMaxZoomPreference(6, 17)
|
||||
: const MinMaxZoomPreference(6, 18),
|
||||
|
||||
onLongPress: (argument) {
|
||||
// ✅ Destination Selection on Long Press
|
||||
onLongPress: (LatLng argument) {
|
||||
MyDialog().getDialog('Are you want to go to this site'.tr, '',
|
||||
() async {
|
||||
controller.clearPolyline();
|
||||
// Ensure we have car data available before routing
|
||||
if (controller.dataCarsLocationByPassenger != null) {
|
||||
await controller.getDirectionMap(
|
||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
||||
'${argument.latitude},${argument.longitude}',
|
||||
);
|
||||
Get.back();
|
||||
Get.back(); // Close Dialog
|
||||
await controller.bottomSheet();
|
||||
controller.showBottomSheet1();
|
||||
} else {
|
||||
@@ -59,52 +60,21 @@ class GoogleMapPassengerWidget extends StatelessWidget {
|
||||
.tr,
|
||||
'',
|
||||
colorText: AppColor.redColor,
|
||||
duration: const Duration(seconds: 11),
|
||||
instantInit: true,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
duration: const Duration(seconds: 5),
|
||||
backgroundColor: AppColor.secondaryColor,
|
||||
icon: const Icon(Icons.error, color: AppColor.redColor),
|
||||
titleText: Text('Error'.tr,
|
||||
style: const TextStyle(color: AppColor.redColor)),
|
||||
messageText: Text(
|
||||
'We Are Sorry That we dont have cars in your Location!'
|
||||
.tr,
|
||||
style: AppStyle.title),
|
||||
icon: const Icon(Icons.error),
|
||||
shouldIconPulse: true,
|
||||
maxWidth: double.infinity,
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
borderRadius: 8,
|
||||
borderColor: AppColor.redColor,
|
||||
borderWidth: 2,
|
||||
backgroundColor: AppColor.secondaryColor,
|
||||
leftBarIndicatorColor: AppColor.redColor,
|
||||
boxShadows: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.25),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 2,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
backgroundGradient: const LinearGradient(
|
||||
colors: [AppColor.redColor, AppColor.accentColor],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
isDismissible: true,
|
||||
showProgressIndicator: false,
|
||||
dismissDirection: DismissDirection.up,
|
||||
snackStyle: SnackStyle.GROUNDED,
|
||||
forwardAnimationCurve: Curves.easeInToLinear,
|
||||
reverseAnimationCurve: Curves.easeInOut,
|
||||
animationDuration: const Duration(milliseconds: 4000),
|
||||
barBlur: 8,
|
||||
overlayColor: AppColor.primaryColor.withOpacity(0.5),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ✅ Hide UI elements on tap
|
||||
onTap: (argument) {
|
||||
controller.hidePlaces();
|
||||
},
|
||||
@@ -114,71 +84,66 @@ class GoogleMapPassengerWidget extends StatelessWidget {
|
||||
zoom: controller.lowPerf ? 14.5 : 15,
|
||||
),
|
||||
|
||||
// ✅ ماركرز (احرص أن الأيقونات محجّمة ومخزّنة Cache في الكنترولر)
|
||||
// ✅ Markers
|
||||
markers: controller.markers.toSet(),
|
||||
|
||||
// ✅ بوليغونز كما هي
|
||||
// ✅ Polygons (e.g., University/Country borders)
|
||||
polygons: controller.polygons,
|
||||
|
||||
// ✅ Polyline مُبسّطة للأجهزة الضعيفة (الكنترولر يجهّز مجموعة مبسطة عند lowPerf)
|
||||
// ✅ Polylines: Switch to lighter version if lowPerf is detected
|
||||
polylines: controller.lowPerf
|
||||
? controller.polyLinesLight
|
||||
.toSet() // <- استخدم مجموعة خفيفة
|
||||
? controller.polyLinesLight.toSet()
|
||||
: controller.polyLines.toSet(),
|
||||
|
||||
// ✅ دوائر خفيفة على الأجهزة الضعيفة
|
||||
// circles: {
|
||||
// Circle(
|
||||
// circleId: const CircleId('circle_id'),
|
||||
// center: controller.passengerLocation,
|
||||
// radius: controller.lowPerf ? 80 : 100,
|
||||
// fillColor:
|
||||
// Colors.blue.withOpacity(controller.lowPerf ? 0.2 : 0.3),
|
||||
// strokeColor: Colors.blue,
|
||||
// strokeWidth: controller.lowPerf ? 1 : 2,
|
||||
// ),
|
||||
// },
|
||||
|
||||
// ✅ الوضع الخفيف: liteMode + تعطيل الطبقات المكلفة + خريطة Normal
|
||||
// ✅ Map Type: Switch to Normal map on low-end devices to save RAM
|
||||
mapType: controller.lowPerf
|
||||
? MapType.normal
|
||||
: (controller.mapType
|
||||
? MapType.satellite
|
||||
: MapType.terrain),
|
||||
: MapType
|
||||
.normal), // Changed terrain default to normal for better performance
|
||||
|
||||
// ✅ UI Settings for Performance
|
||||
myLocationButtonEnabled: false,
|
||||
|
||||
// ⚠️ liteMode (Android فقط): فعّله على الأجهزة الضعيفة
|
||||
// liteModeEnabled: controller.lowPerf,
|
||||
liteModeEnabled: Platform.isAndroid ? isLowEnd() : false,
|
||||
trafficEnabled: controller.mapTrafficON && !isLowEnd(),
|
||||
buildingsEnabled: !isLowEnd(),
|
||||
// ✅ تقليل الكلفة الرسومية
|
||||
|
||||
mapToolbarEnabled: false,
|
||||
rotateGesturesEnabled: isLowEnd() ? false : true,
|
||||
tiltGesturesEnabled: false, // تعطيل الميلان لتقليل الحمل
|
||||
tiltGesturesEnabled:
|
||||
false, // Disable tilt to save GPU resources
|
||||
|
||||
// ✅ Throttle لحركة الكاميرا على الأجهزة الضعيفة
|
||||
onCameraMove: (position) {
|
||||
// Lite Mode (Static image) only on very low-end Androids if needed,
|
||||
// but usually handled by lowPerf logic in mapType/Traffic
|
||||
liteModeEnabled: Platform.isAndroid && controller.lowPerf,
|
||||
|
||||
trafficEnabled: controller.mapTrafficON && !controller.lowPerf,
|
||||
buildingsEnabled: !controller.lowPerf,
|
||||
rotateGesturesEnabled:
|
||||
!controller.lowPerf, // Disable rotation on low-end
|
||||
|
||||
// ✅ Camera Movement Logic
|
||||
onCameraMove: (CameraPosition position) {
|
||||
// 1. Always update current view target (for pickers)
|
||||
controller.newMyLocation = position.target;
|
||||
|
||||
// 2. Handle Drag-to-Select for specific states
|
||||
if (controller.startLocationFromMap == true) {
|
||||
controller.newStartPointLocation = position.target;
|
||||
} else if (controller.passengerStartLocationFromMap == true) {
|
||||
controller.newStartPointLocation = position.target;
|
||||
}
|
||||
|
||||
// 3. Handle Waypoints Dragging
|
||||
int waypointsLength =
|
||||
Get.find<WayPointController>().wayPoints.length;
|
||||
if (waypointsLength > 0 &&
|
||||
controller.wayPointIndex >= 0 &&
|
||||
controller.wayPointIndex <
|
||||
controller.placesCoordinate.length) {
|
||||
controller.placesCoordinate[controller.wayPointIndex] =
|
||||
'${position.target.latitude},${position.target.longitude}';
|
||||
}
|
||||
|
||||
// 4. Throttle heavy calculations (Reverse Geocoding / API calls)
|
||||
if (controller.lowPerf) {
|
||||
controller.onCameraMoveThrottled(position);
|
||||
} else {
|
||||
// منطقك الحالي
|
||||
int waypointsLength =
|
||||
Get.find<WayPointController>().wayPoints.length;
|
||||
int index = controller.wayPointIndex;
|
||||
if (waypointsLength > 0) {
|
||||
controller.placesCoordinate[index] =
|
||||
'${position.target.latitude},${position.target.longitude}';
|
||||
}
|
||||
if (controller.startLocationFromMap == true) {
|
||||
controller.newStartPointLocation = position.target;
|
||||
} else if (controller.passengerStartLocationFromMap ==
|
||||
true) {
|
||||
controller.newStartPointLocation = position.target;
|
||||
}
|
||||
controller.newMyLocation = position.target;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -76,11 +76,11 @@ GetBuilder<MapPassengerController> leftMainMenuIcons() {
|
||||
tooltip: 'VIP Waiting Page',
|
||||
onPressed: () => Get.to(() => VipWaittingPage()),
|
||||
),
|
||||
_buildMapActionButton(
|
||||
icon: Octicons.ellipsis,
|
||||
tooltip: 'test',
|
||||
onPressed: () => Get.to(() => TestPage()),
|
||||
),
|
||||
// _buildMapActionButton(
|
||||
// icon: Octicons.ellipsis,
|
||||
// tooltip: 'test',
|
||||
// onPressed: () => Get.to(() => TestPage()),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -129,18 +129,22 @@ class TestPage extends StatelessWidget {
|
||||
body: Center(
|
||||
child: TextButton(
|
||||
onPressed: () async {
|
||||
var token = (box.read(BoxName.tokenFCM).toString());
|
||||
Log.print(
|
||||
'box.read(BoxName.tokenFCM).toString(): ${box.read(BoxName.tokenFCM).toString()}');
|
||||
// 'e-EE5Z5Fn0x5s6EYbtgT6f:APA91bHBTxkbdljuvDF0iPhso58r7fCwGh-WcYh3CYfUJEShUKFcQf496Xc5E6LHqRFKfOQBxYrWSdLO8d9gLbL-IdgyDuZ7jNUjzvrcV_YmagDtgz7-UNw';
|
||||
// 'fdN1o8akwURHj47wvShC4T:APA91bFm-mFfFjdCbHsDReN0MzPE1hiaHKtPJnzayMec6LiInjzk6YCX41SeF0T1FE7Z6d4Hjy1AkZhLIeebSgX4RrodzwSwZSH0kboTQEfqkrjrk4xw9aM';
|
||||
NotificationService.sendNotification(
|
||||
target: token,
|
||||
title: 'Hi ,I will go now'.tr,
|
||||
body: 'A passenger is waiting for you.',
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'ding',
|
||||
);
|
||||
// var token = (box.read(BoxName.tokenFCM).toString());
|
||||
// Log.print(
|
||||
// 'box.read(BoxName.tokenFCM).toString(): ${box.read(BoxName.tokenFCM).toString()}');
|
||||
// // 'e-EE5Z5Fn0x5s6EYbtgT6f:APA91bHBTxkbdljuvDF0iPhso58r7fCwGh-WcYh3CYfUJEShUKFcQf496Xc5E6LHqRFKfOQBxYrWSdLO8d9gLbL-IdgyDuZ7jNUjzvrcV_YmagDtgz7-UNw';
|
||||
// // 'fdN1o8akwURHj47wvShC4T:APA91bFm-mFfFjdCbHsDReN0MzPE1hiaHKtPJnzayMec6LiInjzk6YCX41SeF0T1FE7Z6d4Hjy1AkZhLIeebSgX4RrodzwSwZSH0kboTQEfqkrjrk4xw9aM';
|
||||
// NotificationService.sendNotification(
|
||||
// target:
|
||||
// 'eznj5vRWRnqwKNtKJBaYNg:APA91bHhJ2DJ1KQa3KRx6wQtX8BkFHq6I_-dXGxT16p6pnV5AwI0bWOeiTJOI35VfTBaK4YSCKmAB4SsRnpARK0MTJ96xtpPmwAKfkvsZFga8OoGMeb3PmA',
|
||||
// title: 'Order',
|
||||
// body: 'endNameAddress',
|
||||
// isTopic: false,
|
||||
// tone: 'tone1',
|
||||
// category: 'Order', // استخدام الفئة الثابتة
|
||||
// driverList: []);
|
||||
// RideState.driverApplied;
|
||||
// Get.find<MapPassengerController>().Ride
|
||||
},
|
||||
child: Text(
|
||||
"Text Button",
|
||||
|
||||
@@ -480,7 +480,7 @@ class MainBottomMenuMap extends StatelessWidget {
|
||||
|
||||
controller.placeDestinationController.clear();
|
||||
|
||||
controller.showBottomSheet1();
|
||||
// controller.showBottomSheet1();
|
||||
|
||||
controller.changeMainBottomMenuMap();
|
||||
},
|
||||
|
||||
@@ -1,3 +1,288 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:Intaleq/constant/box_name.dart';
|
||||
// import 'package:Intaleq/controller/profile/profile_controller.dart';
|
||||
// import 'package:Intaleq/main.dart';
|
||||
// import 'package:Intaleq/views/home/profile/complaint_page.dart';
|
||||
|
||||
// import '../../../constant/colors.dart';
|
||||
// import '../../../constant/links.dart';
|
||||
// import '../../../constant/style.dart';
|
||||
// import '../../../controller/functions/audio_record1.dart';
|
||||
// import '../../../controller/functions/launch.dart';
|
||||
// import '../../../controller/functions/toast.dart';
|
||||
// import '../../../controller/home/map_passenger_controller.dart';
|
||||
|
||||
// // --- الويدجت الرئيسية بالتصميم الجديد ---
|
||||
// class RideBeginPassenger extends StatelessWidget {
|
||||
// const RideBeginPassenger({super.key});
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// // --- نفس منطق استدعاء الكنترولرز ---
|
||||
// final ProfileController profileController = Get.put(ProfileController());
|
||||
// final AudioRecorderController audioController =
|
||||
// Get.put(AudioRecorderController());
|
||||
|
||||
// return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
// // --- نفس شرط الإظهار الخاص بك ---
|
||||
// if (controller.statusRide != 'Begin') {
|
||||
// return const SizedBox.shrink();
|
||||
// }
|
||||
|
||||
// return Positioned(
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// bottom: 0,
|
||||
// child: Container(
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColor.secondaryColor,
|
||||
// borderRadius: const BorderRadius.only(
|
||||
// topLeft: Radius.circular(24),
|
||||
// topRight: Radius.circular(24),
|
||||
// ),
|
||||
// boxShadow: [
|
||||
// BoxShadow(
|
||||
// color: Colors.black.withOpacity(0.2),
|
||||
// blurRadius: 20,
|
||||
// offset: const Offset(0, -5),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// // مقبض السحب (Handle)
|
||||
// Container(
|
||||
// width: 40,
|
||||
// height: 5,
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColor.writeColor.withOpacity(0.3),
|
||||
// borderRadius: BorderRadius.circular(12),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 12),
|
||||
|
||||
// // --- 1. قسم معلومات السائق ---
|
||||
// _buildDriverInfo(controller),
|
||||
// const Divider(height: 24, thickness: 0.5),
|
||||
|
||||
// // --- 2. قسم تقدم الرحلة ---
|
||||
// _buildTripProgress(controller),
|
||||
// const SizedBox(height: 16),
|
||||
|
||||
// // --- 3. قسم الإجراءات والأمان ---
|
||||
// _buildActionButtons(
|
||||
// context, controller, profileController, audioController),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
// // --- ويدجت مساعدة لعرض معلومات السائق بشكل منظم ---
|
||||
// Widget _buildDriverInfo(MapPassengerController controller) {
|
||||
// return Row(
|
||||
// children: [
|
||||
// CircleAvatar(
|
||||
// radius: 28,
|
||||
// backgroundImage: NetworkImage(
|
||||
// '${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'),
|
||||
// ),
|
||||
// const SizedBox(width: 12),
|
||||
// Expanded(
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Text(controller.driverName,
|
||||
// style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
// const SizedBox(height: 2),
|
||||
// Text(
|
||||
// '${controller.make} ${controller.model} • ${box.read(BoxName.carType)}',
|
||||
// style: AppStyle.subtitle
|
||||
// .copyWith(color: AppColor.writeColor.withOpacity(0.7)),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 12),
|
||||
// Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.end,
|
||||
// children: [
|
||||
// Container(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColor.writeColor.withOpacity(0.1),
|
||||
// borderRadius: BorderRadius.circular(6),
|
||||
// ),
|
||||
// child: Text(
|
||||
// controller.licensePlate,
|
||||
// style: AppStyle.subtitle
|
||||
// .copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.5),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 4),
|
||||
// Row(
|
||||
// children: [
|
||||
// Text(controller.driverRate,
|
||||
// style: AppStyle.subtitle
|
||||
// .copyWith(fontWeight: FontWeight.bold)),
|
||||
// const SizedBox(width: 2),
|
||||
// const Icon(Icons.star_rounded,
|
||||
// color: AppColor.yellowColor, size: 16),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// )
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
|
||||
// // --- ويدجت مساعدة لعرض شريط التقدم ---
|
||||
// Widget _buildTripProgress(MapPassengerController controller) {
|
||||
// return Column(
|
||||
// children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Text('Time to Destination'.tr, style: AppStyle.subtitle),
|
||||
// Text(controller.stringRemainingTimeRideBegin,
|
||||
// style: AppStyle.subtitle.copyWith(
|
||||
// fontWeight: FontWeight.bold, color: AppColor.primaryColor)),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(height: 8),
|
||||
// ClipRRect(
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// child: LinearProgressIndicator(
|
||||
// backgroundColor: AppColor.primaryColor.withOpacity(0.2),
|
||||
// color: controller.remainingTimeTimerRideBegin < 60
|
||||
// ? AppColor.redColor
|
||||
// : AppColor.greenColor,
|
||||
// minHeight: 10,
|
||||
// value: controller.progressTimerRideBegin.toDouble(),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
|
||||
// // --- ويدجت مساعدة لعرض أزرار الإجراءات ---
|
||||
// Widget _buildActionButtons(
|
||||
// BuildContext context,
|
||||
// MapPassengerController controller,
|
||||
// ProfileController profileController,
|
||||
// AudioRecorderController audioController) {
|
||||
// return Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
// children: [
|
||||
// _buildActionButton(
|
||||
// icon: Icons.sos_rounded,
|
||||
// label: 'SOS'.tr,
|
||||
// color: AppColor.redColor,
|
||||
// onTap: () async {
|
||||
// // --- نفس منطقك القديم ---
|
||||
// if (box.read(BoxName.sosPhonePassenger) == null) {
|
||||
// await profileController.updatField(
|
||||
// 'sosPhone', TextInputType.phone);
|
||||
// box.write(BoxName.sosPhonePassenger,
|
||||
// profileController.prfoileData['sosPhone']);
|
||||
// } else {
|
||||
// makePhoneCall('112');
|
||||
// }
|
||||
// }),
|
||||
// _buildActionButton(
|
||||
// icon: FontAwesome.whatsapp,
|
||||
// label: 'WhatsApp'.tr,
|
||||
// color: AppColor.greenColor,
|
||||
// onTap: () async {
|
||||
// // --- نفس منطقك القديم ---
|
||||
// if (box.read(BoxName.sosPhonePassenger) == null ||
|
||||
// box.read(BoxName.sosPhonePassenger) == 'sos') {
|
||||
// await profileController.updatField(
|
||||
// 'sosPhone', TextInputType.phone);
|
||||
// box.write(BoxName.sosPhonePassenger,
|
||||
// profileController.prfoileData['sosPhone']);
|
||||
// } else {
|
||||
// final phoneNumber =
|
||||
// box.read(BoxName.sosPhonePassenger).toString();
|
||||
|
||||
// final phone = controller.formatSyrianPhoneNumber(phoneNumber);
|
||||
// controller.sendWhatsapp(phone); //
|
||||
// }
|
||||
// }),
|
||||
// _buildActionButton(
|
||||
// icon: Icons.share_location_outlined, // أيقونة جديدة ومناسبة
|
||||
// label: 'Share'.tr, // اسم جديد وواضح
|
||||
// color: AppColor.blueColor,
|
||||
// onTap: () async {
|
||||
// // نفس الوظيفة السابقة التي كانت تحت اسم "Video Call"
|
||||
// await controller.getTokenForParent();
|
||||
// }),
|
||||
// _buildActionButton(
|
||||
// icon: audioController.isRecording
|
||||
// ? Icons.mic_off_rounded
|
||||
// : Icons.mic_none_rounded,
|
||||
// label: audioController.isRecording ? 'Stop'.tr : 'Record'.tr,
|
||||
// color: AppColor.primaryColor,
|
||||
// onTap: () async {
|
||||
// // --- نفس منطقك القديم ---
|
||||
// if (audioController.isRecording == false) {
|
||||
// await audioController.startRecording();
|
||||
// Toast.show(context, 'Start Record'.tr, AppColor.greenColor);
|
||||
// } else {
|
||||
// await audioController.stopRecording();
|
||||
// Toast.show(context, 'Record saved'.tr, AppColor.greenColor);
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// _buildActionButton(
|
||||
// icon: Icons.note_add_outlined,
|
||||
// label: 'Complaint'.tr,
|
||||
// color: AppColor.yellowColor,
|
||||
// onTap: () {
|
||||
// // --- نفس منطقك القديم ---
|
||||
// Get.to(() => ComplaintPage(), transition: Transition.downToUp);
|
||||
// }),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
|
||||
// // --- ويدجت مساعدة لبناء زر إجراء فردي ---
|
||||
// Widget _buildActionButton(
|
||||
// {required IconData icon,
|
||||
// required String label,
|
||||
// required Color color,
|
||||
// required VoidCallback onTap}) {
|
||||
// return InkWell(
|
||||
// onTap: onTap,
|
||||
// borderRadius: BorderRadius.circular(12),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(4.0),
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// Container(
|
||||
// padding: const EdgeInsets.all(12),
|
||||
// decoration: BoxDecoration(
|
||||
// color: color.withOpacity(0.1),
|
||||
// shape: BoxShape.circle,
|
||||
// ),
|
||||
// child: Icon(icon, color: color, size: 26),
|
||||
// ),
|
||||
// const SizedBox(height: 6),
|
||||
// Text(label, style: AppStyle.subtitle.copyWith(fontSize: 12)),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -5,6 +290,7 @@ import 'package:Intaleq/constant/box_name.dart';
|
||||
import 'package:Intaleq/controller/profile/profile_controller.dart';
|
||||
import 'package:Intaleq/main.dart';
|
||||
import 'package:Intaleq/views/home/profile/complaint_page.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
@@ -14,67 +300,80 @@ import '../../../controller/functions/launch.dart';
|
||||
import '../../../controller/functions/toast.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
|
||||
// --- الويدجت الرئيسية بالتصميم الجديد ---
|
||||
class RideBeginPassenger extends StatelessWidget {
|
||||
const RideBeginPassenger({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// --- نفس منطق استدعاء الكنترولرز ---
|
||||
final ProfileController profileController = Get.put(ProfileController());
|
||||
final AudioRecorderController audioController =
|
||||
Get.put(AudioRecorderController());
|
||||
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
// --- نفس شرط الإظهار الخاص بك ---
|
||||
if (controller.statusRide != 'Begin') {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Obx(() {
|
||||
final controller = Get.find<MapPassengerController>();
|
||||
|
||||
return Positioned(
|
||||
// شرط الإظهار: تظهر فقط عندما تكون الرحلة جارية
|
||||
final bool isVisible =
|
||||
controller.currentRideState.value == RideState.inProgress &&
|
||||
controller.isStartAppHasRide == false;
|
||||
;
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeOutBack, // حركة أكثر سلاسة
|
||||
bottom: isVisible ? 0 : -Get.height * 0.6,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
color: Colors.white, // خلفية بيضاء لنظافة التصميم
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(24),
|
||||
topRight: Radius.circular(24),
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 20,
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 25,
|
||||
spreadRadius: 5,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// مقبض السحب (Handle)
|
||||
// مقبض السحب
|
||||
Container(
|
||||
width: 40,
|
||||
width: 50,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.writeColor.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// --- 1. قسم معلومات السائق ---
|
||||
_buildDriverInfo(controller),
|
||||
const Divider(height: 24, thickness: 0.5),
|
||||
// الصف العلوي: معلومات السائق + السعر المثبت
|
||||
_buildDriverAndPriceSection(controller),
|
||||
|
||||
// --- 2. قسم تقدم الرحلة ---
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// الصف الأوسط: لوحة السيارة الواقعية + نوع السيارة
|
||||
_buildCarInfoSection(controller),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// شريط التقدم والوقت
|
||||
_buildTripProgress(controller),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// --- 3. قسم الإجراءات والأمان ---
|
||||
const SizedBox(height: 20),
|
||||
const Divider(thickness: 1, color: Color(0xFFEEEEEE)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// الأزرار
|
||||
_buildActionButtons(
|
||||
context, controller, profileController, audioController),
|
||||
],
|
||||
@@ -85,86 +384,227 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
});
|
||||
}
|
||||
|
||||
// --- ويدجت مساعدة لعرض معلومات السائق بشكل منظم ---
|
||||
Widget _buildDriverInfo(MapPassengerController controller) {
|
||||
// ويدجت معلومات السائق والسعر
|
||||
Widget _buildDriverAndPriceSection(MapPassengerController controller) {
|
||||
return Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'),
|
||||
// صورة السائق
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColor.primaryColor, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: 15),
|
||||
|
||||
// الاسم والتقييم
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(controller.driverName,
|
||||
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${controller.make} ${controller.model} • ${box.read(BoxName.carType)}',
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(color: AppColor.writeColor.withOpacity(0.7)),
|
||||
controller.driverName,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 18,
|
||||
color: Colors.black87,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.star_rounded,
|
||||
color: AppColor.yellowColor, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
controller.driverRate,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
fontWeight: FontWeight.bold, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.writeColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
|
||||
// السعر المثبت (تصميم كارد للسعر)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.primaryColor.withOpacity(0.2)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text('Total'.tr,
|
||||
style: TextStyle(fontSize: 10, color: Colors.grey[600])),
|
||||
Text(
|
||||
'${NumberFormat('#,###').format(controller.totalPassenger)} 💰',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 18,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
controller.licensePlate,
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(controller.driverRate,
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(width: 2),
|
||||
const Icon(Icons.star_rounded,
|
||||
color: AppColor.yellowColor, size: 16),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// --- ويدجت مساعدة لعرض شريط التقدم ---
|
||||
// ويدجت معلومات السيارة ولوحة الأرقام
|
||||
Widget _buildCarInfoSection(MapPassengerController controller) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF9F9F9),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// نوع السيارة وموديلها
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${controller.make} ${controller.model}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Colors.black87),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
box.read(BoxName.carType) == 'Speed'
|
||||
? 'Fixed Price'
|
||||
.tr // سيظهر "سعر ثابت" (تأكد من إضافتها لملف الترجمة)
|
||||
: box.read(BoxName.carType) ?? 'Car',
|
||||
style: const TextStyle(fontSize: 12, color: Colors.black54),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// -------------------------------------------
|
||||
// تصميم لوحة السيارة الواقعي (Realistic Plate)
|
||||
// -------------------------------------------
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Colors.black, width: 2), // إطار أسود
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(2, 2)),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// الشريط الأزرق الجانبي (مثل اللوحات الدولية)
|
||||
Container(
|
||||
width: 15,
|
||||
height: 35, // ارتفاع اللوحة
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF003399), // أزرق غامق
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(2),
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
"SY", // رمز الدولة (مثال)
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// رقم اللوحة
|
||||
Text(
|
||||
controller.licensePlate, // رقم اللوحة من الكونترولر
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 2.0, // تباعد الأحرف لتبدو كأرقام محفورة
|
||||
fontFamily: 'monospace', // خط ثابت العرض ليشبه اللوحات
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTripProgress(MapPassengerController controller) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Time to Destination'.tr, style: AppStyle.subtitle),
|
||||
Text(controller.stringRemainingTimeRideBegin,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
fontWeight: FontWeight.bold, color: AppColor.primaryColor)),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.access_time_filled,
|
||||
size: 16, color: Colors.grey),
|
||||
const SizedBox(width: 5),
|
||||
Text('Arriving in'.tr,
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 13)),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
controller.stringRemainingTimeRideBegin,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 10),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.2),
|
||||
backgroundColor: Colors.grey[200],
|
||||
color: controller.remainingTimeTimerRideBegin < 60
|
||||
? AppColor.redColor
|
||||
: AppColor.greenColor,
|
||||
minHeight: 10,
|
||||
: AppColor.primaryColor,
|
||||
minHeight: 8,
|
||||
value: controller.progressTimerRideBegin.toDouble(),
|
||||
),
|
||||
),
|
||||
@@ -172,21 +612,21 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// --- ويدجت مساعدة لعرض أزرار الإجراءات ---
|
||||
Widget _buildActionButtons(
|
||||
BuildContext context,
|
||||
MapPassengerController controller,
|
||||
ProfileController profileController,
|
||||
AudioRecorderController audioController) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// زر SOS بتصميم تحذيري
|
||||
_buildActionButton(
|
||||
icon: Icons.sos_rounded,
|
||||
label: 'SOS'.tr,
|
||||
color: AppColor.redColor,
|
||||
label: 'SOS',
|
||||
iconColor: Colors.white,
|
||||
bgColor: AppColor.redColor,
|
||||
onTap: () async {
|
||||
// --- نفس منطقك القديم ---
|
||||
if (box.read(BoxName.sosPhonePassenger) == null) {
|
||||
await profileController.updatField(
|
||||
'sosPhone', TextInputType.phone);
|
||||
@@ -196,12 +636,14 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
makePhoneCall('112');
|
||||
}
|
||||
}),
|
||||
|
||||
// زر واتساب
|
||||
_buildActionButton(
|
||||
icon: FontAwesome.whatsapp,
|
||||
label: 'WhatsApp'.tr,
|
||||
color: AppColor.greenColor,
|
||||
label: 'WhatsApp',
|
||||
iconColor: Colors.white,
|
||||
bgColor: const Color(0xFF25D366),
|
||||
onTap: () async {
|
||||
// --- نفس منطقك القديم ---
|
||||
if (box.read(BoxName.sosPhonePassenger) == null ||
|
||||
box.read(BoxName.sosPhonePassenger) == 'sos') {
|
||||
await profileController.updatField(
|
||||
@@ -211,75 +653,105 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
} else {
|
||||
final phoneNumber =
|
||||
box.read(BoxName.sosPhonePassenger).toString();
|
||||
|
||||
final phone = controller.formatSyrianPhoneNumber(phoneNumber);
|
||||
controller.sendWhatsapp(phone); //
|
||||
controller.sendWhatsapp(phone);
|
||||
}
|
||||
}),
|
||||
|
||||
// زر المشاركة
|
||||
_buildActionButton(
|
||||
icon: Icons.share_location_outlined, // أيقونة جديدة ومناسبة
|
||||
label: 'Share'.tr, // اسم جديد وواضح
|
||||
color: AppColor.blueColor,
|
||||
icon: Icons.share_location_rounded,
|
||||
label: 'Share',
|
||||
iconColor: AppColor.primaryColor,
|
||||
bgColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
onTap: () async {
|
||||
// نفس الوظيفة السابقة التي كانت تحت اسم "Video Call"
|
||||
await controller.getTokenForParent();
|
||||
await controller.shareTripWithFamily();
|
||||
}),
|
||||
_buildActionButton(
|
||||
icon: audioController.isRecording
|
||||
? Icons.mic_off_rounded
|
||||
: Icons.mic_none_rounded,
|
||||
label: audioController.isRecording ? 'Stop'.tr : 'Record'.tr,
|
||||
color: AppColor.primaryColor,
|
||||
onTap: () async {
|
||||
// --- نفس منطقك القديم ---
|
||||
if (audioController.isRecording == false) {
|
||||
await audioController.startRecording();
|
||||
Toast.show(context, 'Start Record'.tr, AppColor.greenColor);
|
||||
} else {
|
||||
await audioController.stopRecording();
|
||||
Toast.show(context, 'Record saved'.tr, AppColor.greenColor);
|
||||
}
|
||||
|
||||
// زر التسجيل
|
||||
GetBuilder<AudioRecorderController>(
|
||||
init: audioController,
|
||||
builder: (audioCtx) {
|
||||
return _buildActionButton(
|
||||
icon: audioCtx.isRecording ? Icons.stop : Icons.mic,
|
||||
label: audioCtx.isRecording ? 'Stop' : 'Record',
|
||||
iconColor:
|
||||
audioCtx.isRecording ? Colors.white : AppColor.primaryColor,
|
||||
bgColor: audioCtx.isRecording
|
||||
? AppColor.redColor
|
||||
: AppColor.primaryColor.withOpacity(0.1),
|
||||
isRecordingAnimation: audioCtx.isRecording,
|
||||
onTap: () async {
|
||||
if (audioCtx.isRecording == false) {
|
||||
await audioCtx.startRecording();
|
||||
Toast.show(context, 'Start Record'.tr, AppColor.greenColor);
|
||||
} else {
|
||||
await audioCtx.stopRecording();
|
||||
Toast.show(context, 'Record saved'.tr, AppColor.greenColor);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// زر الشكوى
|
||||
_buildActionButton(
|
||||
icon: Icons.note_add_outlined,
|
||||
label: 'Complaint'.tr,
|
||||
color: AppColor.yellowColor,
|
||||
icon: Icons.report_gmailerrorred_rounded,
|
||||
label: 'Report'.tr,
|
||||
iconColor: Colors.grey[700]!,
|
||||
bgColor: Colors.grey[200]!,
|
||||
onTap: () {
|
||||
// --- نفس منطقك القديم ---
|
||||
Get.to(() => ComplaintPage(), transition: Transition.downToUp);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// --- ويدجت مساعدة لبناء زر إجراء فردي ---
|
||||
Widget _buildActionButton(
|
||||
{required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
required VoidCallback onTap}) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: color, size: 26),
|
||||
Widget _buildActionButton({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color iconColor,
|
||||
required Color bgColor,
|
||||
required VoidCallback onTap,
|
||||
bool isRecordingAnimation = false,
|
||||
}) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: isRecordingAnimation
|
||||
? Border.all(color: AppColor.redColor, width: 2)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: bgColor.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(label, style: AppStyle.subtitle.copyWith(fontSize: 12)),
|
||||
],
|
||||
child: Icon(icon, color: iconColor, size: 24),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:Intaleq/controller/functions/launch.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
// ... استيراد ملفاتك الأخرى ...
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/profile/profile_controller.dart';
|
||||
@@ -14,161 +17,299 @@ class RideFromStartApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ProfileController profileController = Get.put(ProfileController());
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
return (controller.statusRideFromStart
|
||||
// || controller.statusRide == 'Begin'
|
||||
)
|
||||
? Positioned(
|
||||
left: 10,
|
||||
right: 10,
|
||||
bottom: 4,
|
||||
child: Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
height: Get.height * .3,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Container(
|
||||
width: Get.width * .15,
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'⏱️',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
box.read(BoxName.arrivalTime),
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: Get.width * .15,
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'📍',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
controller.rideStatusFromStartApp['data']
|
||||
['distance']
|
||||
.toString(),
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: Get.width * .15,
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'💵 ',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
controller.rideStatusFromStartApp['data']
|
||||
['price'],
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
final profileController = Get.put(ProfileController());
|
||||
final MapPassengerController controller =
|
||||
Get.find<MapPassengerController>();
|
||||
|
||||
return Obx(() {
|
||||
final bool isRideActive =
|
||||
controller.currentRideState.value == RideState.inProgress &&
|
||||
controller.isStartAppHasRide == true;
|
||||
|
||||
if (!isRideActive) return const SizedBox();
|
||||
|
||||
// قراءة البيانات
|
||||
final rideData = controller.rideStatusFromStartApp['data'] ?? {};
|
||||
final driverId = rideData['driver_id'];
|
||||
final driverName = rideData['driverName'] ?? 'Captain'.tr;
|
||||
final driverRate = controller.driverRate;
|
||||
final carType = rideData['carType'] ?? 'Car'.tr;
|
||||
final carModel = controller.model ?? '';
|
||||
final arrivalTime = box.read(BoxName.arrivalTime) ?? '--:--';
|
||||
|
||||
// تحديد البيانات للعرض
|
||||
final displayTime = controller.stringRemainingTimeRideBegin.isNotEmpty
|
||||
? controller.stringRemainingTimeRideBegin
|
||||
: arrivalTime;
|
||||
final displayDistance = rideData['distance']?.toStringAsFixed(1) ?? 'N/A';
|
||||
final displayPrice = rideData['price']?.toString() ?? 'N/A';
|
||||
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0, // ملتصق بالأسفل تماماً
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white, // خلفية بيضاء نظيفة
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(25),
|
||||
topRight: Radius.circular(25),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 15.0,
|
||||
spreadRadius: 5.0,
|
||||
offset: Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// 1. مقبض صغير للدلالة على السحب (تصميم جمالي)
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 2. حالة الرحلة + معلومات السائق
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// صورة السائق
|
||||
Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border:
|
||||
Border.all(color: AppColor.primaryColor, width: 2),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
child: CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/$driverId.jpg'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// الاسم والسيارة
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundImage: NetworkImage(
|
||||
// '',
|
||||
// ),
|
||||
'https://ride.mobile-app.store/portrate_captain_image/${controller.rideStatusFromStartApp['data']['driver_id']}.jpg'),
|
||||
),
|
||||
Text(
|
||||
'${controller.rideStatusFromStartApp['data']['driverName']}',
|
||||
style: AppStyle.title,
|
||||
driverName,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Column(
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.star,
|
||||
color: AppColor.yellowColor, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${controller.rideStatusFromStartApp['data']['rateDriver']} 📈',
|
||||
style: AppStyle.title,
|
||||
driverRate,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 13, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(width: 1, height: 12, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${controller.rideStatusFromStartApp['data']['carType']}',
|
||||
style: AppStyle.title,
|
||||
"$carType - $carModel",
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 13, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
if (box.read(BoxName.sosPhonePassenger) == null) {
|
||||
{
|
||||
await profileController.updatField(
|
||||
'sosPhone', TextInputType.phone);
|
||||
box.write(BoxName.sosPhonePassenger,
|
||||
profileController.prfoileData['sosPhone']);
|
||||
}
|
||||
} else {
|
||||
controller
|
||||
.sendSMS(box.read(BoxName.sosPhonePassenger));
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.sos_rounded,
|
||||
color: AppColor.redColor,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
if (box.read(BoxName.sosPhonePassenger) == null ||
|
||||
box.read(BoxName.sosPhonePassenger) == 'sos') {
|
||||
{
|
||||
await profileController.updatField(
|
||||
'sosPhone', TextInputType.phone);
|
||||
box.write(BoxName.sosPhonePassenger,
|
||||
profileController.prfoileData['sosPhone']);
|
||||
}
|
||||
} else {
|
||||
String phoneNumber = box
|
||||
.read(BoxName.sosPhonePassenger)
|
||||
.toString();
|
||||
// phoneNumber = phoneNumber.replaceAll('0', '');
|
||||
var phone =
|
||||
// '+${box.read(BoxName.countryCode)}${box.read(BoxName.sosPhonePassenger)}';
|
||||
'${box.read(BoxName.sosPhonePassenger)}';
|
||||
controller.sendWhatsapp(phone);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
FontAwesome.whatsapp,
|
||||
color: AppColor.greenColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
|
||||
// حالة الرحلة (نص ملون)
|
||||
_buildStatusBadge(controller.currentRideState.value),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 3. شريط المعلومات (وقت، مسافة، سعر)
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.grayColor
|
||||
.withOpacity(0.1), // خلفية رمادية خفيفة جداً
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildInfoColumn(
|
||||
Icons.access_time_filled, displayTime, "Time".tr),
|
||||
_buildVerticalDivider(),
|
||||
_buildInfoColumn(Icons.location_on, "$displayDistance KM",
|
||||
"Distance".tr),
|
||||
_buildVerticalDivider(),
|
||||
_buildInfoColumn(
|
||||
Icons.attach_money, "$displayPrice SYP", "Price".tr),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox();
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 4. أزرار التحكم (SOS & Share)
|
||||
Row(
|
||||
children: [
|
||||
// زر المشاركة (يأخذ مساحة أكبر)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _checkAndCall(
|
||||
controller.sendWhatsapp, profileController),
|
||||
icon:
|
||||
const Icon(FontAwesome.whatsapp, color: Colors.white),
|
||||
label: Text("Share Trip".tr,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.greenColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
// زر الاستغاثة SOS
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () =>
|
||||
makePhoneCall(box.read(BoxName.sosPhonePassenger)),
|
||||
icon: const Icon(Icons.sos, color: Colors.white),
|
||||
label: Text("SOS".tr,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.redColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------- Widgets Helper Methods -------------------------
|
||||
|
||||
Widget _buildStatusBadge(RideState status) {
|
||||
String text;
|
||||
Color color;
|
||||
|
||||
if (status == RideState.inProgress) {
|
||||
text = 'On Trip'.tr;
|
||||
color = AppColor.primaryColor;
|
||||
} else if (status == RideState.driverArrived) {
|
||||
text = 'Arrived'.tr;
|
||||
color = AppColor.greenColor;
|
||||
} else {
|
||||
text = 'Coming'.tr;
|
||||
color = AppColor.yellowColor;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withOpacity(0.5)),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoColumn(IconData icon, String value, String label) {
|
||||
return Column(
|
||||
children: [
|
||||
Icon(icon,
|
||||
color: AppColor.secondaryColor,
|
||||
size: 22), // افترضت أن السكندري لون داكن، أو استخدم Primary
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 15,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVerticalDivider() {
|
||||
return Container(
|
||||
height: 30,
|
||||
width: 1,
|
||||
color: Colors.grey[300],
|
||||
);
|
||||
}
|
||||
|
||||
// دالة المساعدة للمنطق (بقيت كما هي ولكن تم تمرير البروفايل كونترولر)
|
||||
Future<void> _checkAndCall(
|
||||
Function(String) action, ProfileController profileController) async {
|
||||
String? sosPhone = box.read(BoxName.sosPhonePassenger);
|
||||
if (sosPhone == null || sosPhone == 'sos') {
|
||||
await profileController.updatField('sosPhone', TextInputType.phone);
|
||||
sosPhone = profileController.prfoileData['sosPhone'];
|
||||
}
|
||||
|
||||
if (sosPhone != null && sosPhone != 'sos') {
|
||||
action(sosPhone);
|
||||
} else {
|
||||
Get.snackbar('Warning'.tr, 'Please set a valid SOS phone number.'.tr,
|
||||
backgroundColor: AppColor.redColor, colorText: Colors.white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:Intaleq/constant/colors.dart';
|
||||
import 'package:Intaleq/constant/style.dart';
|
||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
||||
import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
||||
import 'package:Intaleq/views/widgets/my_textField.dart';
|
||||
|
||||
// --- الويدجت الرئيسية بالتصميم الجديد ---
|
||||
class SearchingCaptainWindow extends StatefulWidget {
|
||||
@@ -36,62 +33,68 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
builder: (controller) {
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
bottom: controller.isSearchingWindow ? 0 : -Get.height * 0.4,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(24),
|
||||
topRight: Radius.circular(24),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// --- 1. أنيميشن الرادار ---
|
||||
_buildRadarAnimation(controller),
|
||||
const SizedBox(height: 20),
|
||||
// [تعديل 1] نستخدم Obx للاستماع إلى التغييرات في حالة الرحلة
|
||||
return Obx(() {
|
||||
// ابحث عن الكنترولر مرة واحدة
|
||||
final controller = Get.find<MapPassengerController>();
|
||||
|
||||
// --- 2. زر الإلغاء ---
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
// --- نفس منطقك للإلغاء ---
|
||||
controller.cancelRide();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColor.writeColor,
|
||||
side: BorderSide(
|
||||
color: AppColor.writeColor.withOpacity(0.3)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text('Cancel Search'.tr),
|
||||
),
|
||||
),
|
||||
],
|
||||
// [تعديل 2] شرط الإظهار يعتمد الآن على حالة الرحلة مباشرة
|
||||
final bool isVisible =
|
||||
controller.currentRideState.value == RideState.searching;
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
bottom: isVisible ? 0 : -Get.height * 0.45, // زيادة الارتفاع قليلاً
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(24),
|
||||
topRight: Radius.circular(24),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// --- 1. أنيميشن الرادار ---
|
||||
_buildRadarAnimation(controller),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// --- 2. زر الإلغاء ---
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
// [تعديل 3] استدعاء دالة الإلغاء الموحدة
|
||||
controller.cancelRide();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColor.writeColor,
|
||||
side:
|
||||
BorderSide(color: AppColor.writeColor.withOpacity(0.3)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text('Cancel Search'.tr),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// --- ويدجت بناء أنيميشن الرادار ---
|
||||
@@ -101,7 +104,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// --- دوائر الرادار المتحركة ---
|
||||
// --- دوائر الرادار المتحركة (تبقى كما هي) ---
|
||||
...List.generate(3, (index) {
|
||||
return FadeTransition(
|
||||
opacity: Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||
@@ -134,6 +137,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
// [تعديل 4] النص يأتي مباشرة من الكنترولر
|
||||
controller.driversStatusForSearchWindow,
|
||||
style: AppStyle.headTitle.copyWith(fontSize: 20),
|
||||
textAlign: TextAlign.center,
|
||||
@@ -146,8 +150,32 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// --- استدعاء نفس دالة المؤقت الخاصة بك ---
|
||||
buildTimerForIncrease(controller),
|
||||
|
||||
// --- [!! تعديل جوهري !!] ---
|
||||
// لم نعد بحاجة لـ buildTimerForIncrease
|
||||
// المؤقت الرئيسي في الكنترولر هو من يقرر متى يعرض الحوار
|
||||
// وهذا الجزء من الواجهة أصبح "غبياً" (لا يحتوي على منطق)
|
||||
// الكنترولر سيستدعي _showIncreaseFeeDialog مباشرة
|
||||
SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
color: AppColor.primaryColor,
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.2),
|
||||
),
|
||||
Center(
|
||||
child: Icon(
|
||||
Icons.search,
|
||||
color: AppColor.writeColor.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -156,128 +184,285 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
||||
}
|
||||
}
|
||||
|
||||
// --- نفس دالة المؤقت الخاصة بك مع تعديلات شكلية بسيطة ---
|
||||
Widget buildTimerForIncrease(MapPassengerController mapPassengerController) {
|
||||
return StreamBuilder<int>(
|
||||
stream: Stream.periodic(const Duration(seconds: 1))
|
||||
.map((_) => ++mapPassengerController.currentTimeSearchingCaptainWindow),
|
||||
initialData: 0,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data! > 30) {
|
||||
// --- عرض زر زيادة الأجرة بنفس منطقك القديم ---
|
||||
return TextButton(
|
||||
onPressed: () =>
|
||||
_showIncreaseFeeDialog(context, mapPassengerController),
|
||||
child: Text(
|
||||
"No one accepted? Try increasing the fare.".tr,
|
||||
style: AppStyle.title.copyWith(
|
||||
color: AppColor.primaryColor,
|
||||
decoration: TextDecoration.underline),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
// --- [!! تعديل جوهري !!] ---
|
||||
// تم حذف دالة `buildTimerForIncrease` بالكامل.
|
||||
// تم حذف دالة `_showIncreaseFeeDialog` من هذا الملف.
|
||||
// لماذا؟ لأن الكنترولر الآن هو المسؤول الوحيد عن إظهار الحوار.
|
||||
// دالة `_showIncreaseFeeDialog` موجودة بالفعل داخل `map_passenger_controller.dart`
|
||||
// وسيتم استدعاؤها من `_handleRideState` عند انتهاء مهلة الـ 90 ثانية.
|
||||
|
||||
final double progress = (snapshot.data ?? 0).toDouble() / 30.0;
|
||||
// // --- الويدجت الرئيسية بالتصميم الجديد ---
|
||||
// class SearchingCaptainWindow extends StatefulWidget {
|
||||
// const SearchingCaptainWindow({super.key});
|
||||
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
value: progress,
|
||||
strokeWidth: 3,
|
||||
color: AppColor.primaryColor,
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.2),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
'${snapshot.data ?? 0}',
|
||||
style: AppStyle.title.copyWith(
|
||||
color: AppColor.writeColor, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
// @override
|
||||
// State<SearchingCaptainWindow> createState() => _SearchingCaptainWindowState();
|
||||
// }
|
||||
|
||||
// --- دالة لعرض نافذة زيادة الأجرة (مأخوذة من منطقك القديم) ---
|
||||
void _showIncreaseFeeDialog(
|
||||
BuildContext context, MapPassengerController mapPassengerController) {
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: "Increase Your Trip Fee (Optional)".tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Column(
|
||||
children: [
|
||||
Text(
|
||||
"We haven't found any drivers yet. Consider increasing your trip fee to make your offer more attractive to drivers."
|
||||
.tr,
|
||||
style: AppStyle.subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
mapPassengerController.increasFeeFromPassenger.text =
|
||||
(mapPassengerController.totalPassenger + 3)
|
||||
.toStringAsFixed(1);
|
||||
mapPassengerController.update();
|
||||
},
|
||||
icon: const Icon(Icons.add_circle,
|
||||
size: 40, color: AppColor.greenColor),
|
||||
),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Form(
|
||||
key: mapPassengerController.increaseFeeFormKey,
|
||||
child: MyTextForm(
|
||||
controller: mapPassengerController.increasFeeFromPassenger,
|
||||
label:
|
||||
mapPassengerController.totalPassenger.toStringAsFixed(2),
|
||||
hint:
|
||||
mapPassengerController.totalPassenger.toStringAsFixed(2),
|
||||
type: TextInputType.number,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
mapPassengerController.increasFeeFromPassenger.text =
|
||||
(mapPassengerController.totalPassenger - 3)
|
||||
.toStringAsFixed(1);
|
||||
mapPassengerController.update();
|
||||
},
|
||||
icon: const Icon(Icons.remove_circle,
|
||||
size: 40, color: AppColor.redColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text("No, thanks".tr,
|
||||
style: const TextStyle(color: AppColor.redColor)),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
mapPassengerController.cancelRide();
|
||||
},
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColor.greenColor),
|
||||
child: Text("Increase Fee".tr),
|
||||
onPressed: () =>
|
||||
mapPassengerController.increaseFeeByPassengerAndReOrder(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
// class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
||||
// with SingleTickerProviderStateMixin {
|
||||
// late AnimationController _animationController;
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// _animationController = AnimationController(
|
||||
// vsync: this,
|
||||
// duration: const Duration(seconds: 2),
|
||||
// )..repeat();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void dispose() {
|
||||
// _animationController.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return GetBuilder<MapPassengerController>(
|
||||
// builder: (controller) {
|
||||
// return AnimatedPositioned(
|
||||
// duration: const Duration(milliseconds: 300),
|
||||
// curve: Curves.easeInOut,
|
||||
// bottom: controller.isSearchingWindow ? 0 : -Get.height * 0.4,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// child: Container(
|
||||
// padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColor.secondaryColor,
|
||||
// borderRadius: const BorderRadius.only(
|
||||
// topLeft: Radius.circular(24),
|
||||
// topRight: Radius.circular(24),
|
||||
// ),
|
||||
// boxShadow: [
|
||||
// BoxShadow(
|
||||
// color: Colors.black.withOpacity(0.2),
|
||||
// blurRadius: 20,
|
||||
// offset: const Offset(0, -5),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// // --- 1. أنيميشن الرادار ---
|
||||
// _buildRadarAnimation(controller),
|
||||
// const SizedBox(height: 20),
|
||||
|
||||
// // --- 2. زر الإلغاء ---
|
||||
// SizedBox(
|
||||
// width: double.infinity,
|
||||
// child: OutlinedButton(
|
||||
// onPressed: () {
|
||||
// // --- نفس منطقك للإلغاء ---
|
||||
// controller.changeCancelRidePageShow();
|
||||
// },
|
||||
// style: OutlinedButton.styleFrom(
|
||||
// foregroundColor: AppColor.writeColor,
|
||||
// side: BorderSide(
|
||||
// color: AppColor.writeColor.withOpacity(0.3)),
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(12)),
|
||||
// padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
// ),
|
||||
// child: Text('Cancel Search'.tr),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
// // --- ويدجت بناء أنيميشن الرادار ---
|
||||
// Widget _buildRadarAnimation(MapPassengerController controller) {
|
||||
// return SizedBox(
|
||||
// height: 180, // ارتفاع ثابت لمنطقة الأنيميشن
|
||||
// child: Stack(
|
||||
// alignment: Alignment.center,
|
||||
// children: [
|
||||
// // --- دوائر الرادار المتحركة ---
|
||||
// ...List.generate(3, (index) {
|
||||
// return FadeTransition(
|
||||
// opacity: Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||
// CurvedAnimation(
|
||||
// parent: _animationController,
|
||||
// curve: Interval((index) / 3, 1.0, curve: Curves.easeInOut),
|
||||
// ),
|
||||
// ),
|
||||
// child: ScaleTransition(
|
||||
// scale: Tween<double>(begin: 0.3, end: 1.0).animate(
|
||||
// CurvedAnimation(
|
||||
// parent: _animationController,
|
||||
// curve: Interval((index) / 3, 1.0, curve: Curves.easeInOut),
|
||||
// ),
|
||||
// ),
|
||||
// child: Container(
|
||||
// decoration: BoxDecoration(
|
||||
// shape: BoxShape.circle,
|
||||
// border: Border.all(
|
||||
// color: AppColor.primaryColor.withOpacity(0.7),
|
||||
// width: 2,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// // --- المحتوى في المنتصف ---
|
||||
// Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// Text(
|
||||
// controller.driversStatusForSearchWindow,
|
||||
// style: AppStyle.headTitle.copyWith(fontSize: 20),
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
// const SizedBox(height: 8),
|
||||
// Text(
|
||||
// 'Searching for the nearest captain...'.tr,
|
||||
// style: AppStyle.subtitle
|
||||
// .copyWith(color: AppColor.writeColor.withOpacity(0.7)),
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
// const SizedBox(height: 16),
|
||||
// // --- استدعاء نفس دالة المؤقت الخاصة بك ---
|
||||
// buildTimerForIncrease(controller),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// // --- نفس دالة المؤقت الخاصة بك مع تعديلات شكلية بسيطة ---
|
||||
// Widget buildTimerForIncrease(MapPassengerController mapPassengerController) {
|
||||
// return StreamBuilder<int>(
|
||||
// stream: Stream.periodic(const Duration(seconds: 1))
|
||||
// .map((_) => ++mapPassengerController.currentTimeSearchingCaptainWindow),
|
||||
// initialData: 0,
|
||||
// builder: (context, snapshot) {
|
||||
// if (snapshot.hasData && snapshot.data! > 45) {
|
||||
// // --- عرض زر زيادة الأجرة بنفس منطقك القديم ---
|
||||
// return TextButton(
|
||||
// onPressed: () =>
|
||||
// _showIncreaseFeeDialog(context, mapPassengerController),
|
||||
// child: Text(
|
||||
// "No one accepted? Try increasing the fare.".tr,
|
||||
// style: AppStyle.title.copyWith(
|
||||
// color: AppColor.primaryColor,
|
||||
// decoration: TextDecoration.underline),
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// final double progress = (snapshot.data ?? 0).toDouble() / 30.0;
|
||||
|
||||
// return SizedBox(
|
||||
// height: 40,
|
||||
// width: 40,
|
||||
// child: Stack(
|
||||
// fit: StackFit.expand,
|
||||
// children: [
|
||||
// CircularProgressIndicator(
|
||||
// value: progress,
|
||||
// strokeWidth: 3,
|
||||
// color: AppColor.primaryColor,
|
||||
// backgroundColor: AppColor.primaryColor.withOpacity(0.2),
|
||||
// ),
|
||||
// Center(
|
||||
// child: Text(
|
||||
// '${snapshot.data ?? 0}',
|
||||
// style: AppStyle.title.copyWith(
|
||||
// color: AppColor.writeColor, fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
// // --- دالة لعرض نافذة زيادة الأجرة (مأخوذة من منطقك القديم) ---
|
||||
// void _showIncreaseFeeDialog(
|
||||
// BuildContext context, MapPassengerController mapPassengerController) {
|
||||
// Get.defaultDialog(
|
||||
// barrierDismissible: false,
|
||||
// title: "Increase Your Trip Fee (Optional)".tr,
|
||||
// titleStyle: AppStyle.title,
|
||||
// content: Column(
|
||||
// children: [
|
||||
// Text(
|
||||
// "We haven't found any drivers yet. Consider increasing your trip fee to make your offer more attractive to drivers."
|
||||
// .tr,
|
||||
// style: AppStyle.subtitle,
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
// const SizedBox(height: 16),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// IconButton(
|
||||
// onPressed: () {
|
||||
// mapPassengerController.increasFeeFromPassenger.text =
|
||||
// (mapPassengerController.totalPassenger + 3)
|
||||
// .toStringAsFixed(1);
|
||||
// mapPassengerController.update();
|
||||
// },
|
||||
// icon: const Icon(Icons.add_circle,
|
||||
// size: 40, color: AppColor.greenColor),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: 100,
|
||||
// child: Form(
|
||||
// key: mapPassengerController.increaseFeeFormKey,
|
||||
// child: MyTextForm(
|
||||
// controller: mapPassengerController.increasFeeFromPassenger,
|
||||
// label:
|
||||
// mapPassengerController.totalPassenger.toStringAsFixed(2),
|
||||
// hint:
|
||||
// mapPassengerController.totalPassenger.toStringAsFixed(2),
|
||||
// type: TextInputType.number,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// IconButton(
|
||||
// onPressed: () {
|
||||
// mapPassengerController.increasFeeFromPassenger.text =
|
||||
// (mapPassengerController.totalPassenger - 3)
|
||||
// .toStringAsFixed(1);
|
||||
// mapPassengerController.update();
|
||||
// },
|
||||
// icon: const Icon(Icons.remove_circle,
|
||||
// size: 40, color: AppColor.redColor),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// actions: [
|
||||
// TextButton(
|
||||
// child: Text("No, thanks".tr,
|
||||
// style: const TextStyle(color: AppColor.redColor)),
|
||||
// onPressed: () {
|
||||
// Get.back();
|
||||
// // mapPassengerController.cancelRide();
|
||||
// mapPassengerController.changeCancelRidePageShow();
|
||||
// },
|
||||
// ),
|
||||
// ElevatedButton(
|
||||
// style: ElevatedButton.styleFrom(backgroundColor: AppColor.greenColor),
|
||||
// child: Text("Increase Fee".tr),
|
||||
// onPressed: () =>
|
||||
// mapPassengerController.increaseFeeByPassengerAndReOrder(),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
|
||||
@@ -237,7 +237,7 @@ class VipRideBeginPassenger extends StatelessWidget {
|
||||
width: Get.width * .15,
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
await controller.getTokenForParent();
|
||||
await controller.shareTripWithFamily();
|
||||
},
|
||||
icon: const Icon(
|
||||
AntDesign.Safety,
|
||||
|
||||
@@ -447,64 +447,37 @@ void showPaymentOptions(BuildContext context, PaymentController controller) {
|
||||
)),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'Insert Wallet phone number'.tr,
|
||||
content: Form(
|
||||
key: controller.formKey,
|
||||
child: MyTextForm(
|
||||
controller: controller.walletphoneController,
|
||||
label: 'Insert Wallet phone number'.tr,
|
||||
hint: '963941234567',
|
||||
type: TextInputType.phone)),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'OK'.tr,
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
if (controller.formKey.currentState!.validate()) {
|
||||
box.write(BoxName.phoneWallet,
|
||||
controller.walletphoneController.text);
|
||||
// await payWithSyriaTelWallet(
|
||||
// context, pricePoint.toString(), 'SYP');
|
||||
bool isAuthSupported =
|
||||
await LocalAuthentication().isDeviceSupported();
|
||||
if (isAuthSupported) {
|
||||
bool didAuthenticate =
|
||||
await LocalAuthentication().authenticate(
|
||||
localizedReason:
|
||||
'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
|
||||
);
|
||||
if (!didAuthenticate) {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
print(
|
||||
"❌ User did not authenticate with biometrics");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Get.to(() => PaymentScreenSmsProvider(
|
||||
amount: controller.selectedAmount!.toDouble()));
|
||||
}
|
||||
}));
|
||||
// التحقق بالبصمة قبل أي شيء
|
||||
bool isAuthSupported =
|
||||
await LocalAuthentication().isDeviceSupported();
|
||||
|
||||
if (isAuthSupported) {
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
|
||||
);
|
||||
|
||||
if (!didAuthenticate) {
|
||||
print("❌ User did not authenticate with biometrics");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// الانتقال مباشرة لإنشاء الفاتورة بعد النجاح بالبصمة
|
||||
Get.to(() => PaymentScreenSmsProvider(
|
||||
amount: double.parse(controller.selectedAmount.toString())));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Pay by Sham Cash'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Image.asset(
|
||||
'assets/images/shamCash.png',
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Pay by Sham Cash'.tr),
|
||||
const SizedBox(width: 10),
|
||||
Image.asset(
|
||||
'assets/images/shamCash.png',
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
|
||||
@@ -1,71 +1,56 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:Intaleq/views/widgets/mydialoug.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_core/src/get_main.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
// خدمة الدفع للراكب (تم تحديث المسارات)
|
||||
class PaymentService {
|
||||
final String _baseUrl = "${AppLink.seferPaymentServer}/sms_webhook";
|
||||
// المسار الجديد لمجلد الركاب
|
||||
final String _baseUrl = "${AppLink.paymentServer}/ride/shamcash/passenger";
|
||||
|
||||
Future<String?> createInvoice({
|
||||
required String userPhone,
|
||||
required double amount,
|
||||
}) async {
|
||||
final url = "$_baseUrl/create_invoice_passenger.php";
|
||||
Future<String?> createInvoice({required double amount}) async {
|
||||
final url = "$_baseUrl/create_invoice.php";
|
||||
try {
|
||||
final response = await CRUD().postWallet(
|
||||
link: url,
|
||||
payload: {
|
||||
'user_phone': userPhone.toString(),
|
||||
'passengerID': box.read(BoxName.passengerID),
|
||||
'passengerID': box.read(BoxName.passengerID), // استخدام passengerID
|
||||
'amount': amount.toString(),
|
||||
},
|
||||
).timeout(const Duration(seconds: 15)); // إضافة مهلة للطلب
|
||||
).timeout(const Duration(seconds: 15));
|
||||
|
||||
if (response != 'failure') {
|
||||
final data = (response);
|
||||
final data = response;
|
||||
if (data['status'] == 'success' && data['invoice_number'] != null) {
|
||||
debugPrint(
|
||||
"تم إنشاء الفاتورة بنجاح. الرقم: ${data['invoice_number']}");
|
||||
return data['invoice_number'].toString();
|
||||
} else {
|
||||
debugPrint("فشل في إنشاء الفاتورة من السيرفر: ${data['message']}");
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
debugPrint("خطأ في السيرفر عند إنشاء الفاتورة: ${response.statusCode}");
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint("حدث استثناء عند إنشاء الفاتورة: $e");
|
||||
debugPrint("Create Invoice Error: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// دالة للتحقق من حالة فاتورة واحدة
|
||||
Future<bool> checkInvoiceStatus(String invoiceNumber) async {
|
||||
final url = "$_baseUrl/check_invoice_status_passenger.php";
|
||||
final url = "$_baseUrl/check_status.php";
|
||||
try {
|
||||
final response = await CRUD().postWallet(link: url, payload: {
|
||||
'invoice_number': invoiceNumber,
|
||||
}).timeout(const Duration(seconds: 10)); // مهلة للشبكة
|
||||
}).timeout(const Duration(seconds: 10));
|
||||
|
||||
if (response != 'failure') {
|
||||
final data = (response);
|
||||
final data = response;
|
||||
return data['status'] == 'success' &&
|
||||
data['invoice_status'] == 'completed';
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint("خطأ أثناء التحقق من الفاتورة: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -83,14 +68,14 @@ class PaymentScreenSmsProvider extends StatefulWidget {
|
||||
final double amount;
|
||||
final String providerName;
|
||||
final String providerLogo;
|
||||
final String paymentPhoneNumber;
|
||||
final String qrImagePath;
|
||||
|
||||
const PaymentScreenSmsProvider({
|
||||
super.key,
|
||||
required this.amount,
|
||||
this.providerName = 'شام كاش',
|
||||
this.providerLogo = 'assets/images/shamCash.png',
|
||||
this.paymentPhoneNumber = '963942542053',
|
||||
this.qrImagePath = 'assets/images/shamcashsend.png',
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -103,7 +88,6 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
Timer? _pollingTimer;
|
||||
PaymentStatus _status = PaymentStatus.creatingInvoice;
|
||||
String? _invoiceNumber;
|
||||
final String phone = box.read(BoxName.phoneWallet);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -113,17 +97,14 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollingTimer?.cancel(); // مهم جداً: إلغاء المؤقت عند الخروج من الشاشة
|
||||
_pollingTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _createAndPollInvoice() async {
|
||||
setState(() => _status = PaymentStatus.creatingInvoice);
|
||||
|
||||
final invoiceNumber = await _paymentService.createInvoice(
|
||||
userPhone: phone,
|
||||
amount: widget.amount,
|
||||
);
|
||||
final invoiceNumber =
|
||||
await _paymentService.createInvoice(amount: widget.amount);
|
||||
|
||||
if (invoiceNumber != null && mounted) {
|
||||
setState(() {
|
||||
@@ -137,7 +118,7 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
}
|
||||
|
||||
void _startPolling(String invoiceNumber) {
|
||||
const timeoutDuration = Duration(minutes: 3);
|
||||
const timeoutDuration = Duration(minutes: 5);
|
||||
var elapsed = Duration.zero;
|
||||
|
||||
_pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
|
||||
@@ -147,72 +128,55 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
if (mounted) setState(() => _status = PaymentStatus.paymentTimeout);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint("Polling... Checking invoice status for: $invoiceNumber");
|
||||
final isCompleted =
|
||||
await _paymentService.checkInvoiceStatus(invoiceNumber);
|
||||
if (isCompleted && mounted) {
|
||||
timer.cancel();
|
||||
setState(() => _status = PaymentStatus.paymentSuccess);
|
||||
// TODO: تحديث رصيد المستخدم أو تنفيذ الإجراءات اللازمة
|
||||
MyDialog().getDialog(
|
||||
'Payment Successful!'.tr,
|
||||
''.tr,
|
||||
() {
|
||||
Get.back();
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// دالة جديدة لمعالجة محاولة الرجوع للخلف
|
||||
void _onPopInvoked(bool didPop) async {
|
||||
// إذا كان الرجوع قد تم بالفعل (مثلاً من خلال Navigator.pop)، لا تفعل شيئاً
|
||||
if (didPop) return;
|
||||
|
||||
// إذا كان المستخدم ينتظر الدفع، أظهر له حوار التأكيد
|
||||
Future<bool> _onPopInvoked() async {
|
||||
if (_status == PaymentStatus.waitingForPayment) {
|
||||
final shouldPop = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('هل أنت متأكد؟'),
|
||||
content: const Text('إذا خرجت الآن، سيتم إلغاء عملية الدفع الحالية.'),
|
||||
title: const Text('إلغاء العملية؟', textAlign: TextAlign.right),
|
||||
content: const Text('الخروج الآن سيؤدي لإلغاء متابعة عملية الدفع.',
|
||||
textAlign: TextAlign.right),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('البقاء'),
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('البقاء')),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('الخروج'),
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('خروج', style: TextStyle(color: Colors.red))),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// إذا وافق المستخدم على الخروج، قم بإغلاق الشاشة
|
||||
if (shouldPop ?? false) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
return shouldPop ?? false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// استخدام PopScope بدلاً من WillPopScope
|
||||
return PopScope(
|
||||
// منع الرجوع التلقائي فقط في حالة انتظار الدفع
|
||||
canPop: _status != PaymentStatus.waitingForPayment,
|
||||
// استدعاء دالة التحقق عند محاولة الرجوع
|
||||
onPopInvoked: _onPopInvoked,
|
||||
return WillPopScope(
|
||||
onWillPop: _onPopInvoked,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: Text("الدفع عبر ${widget.providerName}")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: _buildContentByStatus(),
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
title: Text("دفع عبر ${widget.providerName}"),
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Center(child: _buildContentByStatus()),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -227,7 +191,7 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 20),
|
||||
Text("جاري إنشاء فاتورة الدفع...", style: TextStyle(fontSize: 16)),
|
||||
Text("جاري إنشاء رقم البيان...", style: TextStyle(fontSize: 16)),
|
||||
],
|
||||
);
|
||||
case PaymentStatus.waitingForPayment:
|
||||
@@ -242,94 +206,162 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
|
||||
Widget _buildWaitingForPaymentUI() {
|
||||
final currencyFormat = NumberFormat.decimalPattern('ar_SY');
|
||||
final invoiceText = _invoiceNumber ?? '------';
|
||||
final invoiceText = _invoiceNumber ?? '---';
|
||||
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(widget.providerLogo, width: 96),
|
||||
const SizedBox(height: 16),
|
||||
Text("تعليمات الدفع", style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
Card(
|
||||
elevation: 1.5,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
// 1. المبلغ
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 15),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.blue.shade800, Colors.blue.shade600]),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blue.withOpacity(0.25),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text("المبلغ المطلوب",
|
||||
style: TextStyle(color: Colors.white70, fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
Text("${currencyFormat.format(widget.amount)} ل.س",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// 2. التعليمات والنسخ (للراكب)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.shade100,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50, shape: BoxShape.circle),
|
||||
child: Icon(Icons.priority_high_rounded,
|
||||
color: Colors.orange.shade800, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
"انسخ الرقم أدناه وضعه في خانة (الملاحظات) عند الدفع.",
|
||||
style: TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: invoiceText));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: const Text("تم نسخ رقم البيان ✅",
|
||||
textAlign: TextAlign.center),
|
||||
backgroundColor: Colors.green.shade600));
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 15, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.blue.shade200, width: 1.5)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("رقم البيان (Invoice ID)",
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Colors.grey)),
|
||||
Text(invoiceText,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.5)),
|
||||
],
|
||||
),
|
||||
const Icon(Icons.copy_rounded,
|
||||
color: Colors.blue, size: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// 3. QR Code
|
||||
const Text("امسح الرمز للدفع",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 15),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: InteractiveViewer(
|
||||
child: Image.asset(widget.qrImagePath))));
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.grey.shade300)),
|
||||
child: Column(
|
||||
children: [
|
||||
_StepTile(number: 1, text: "افتح تطبيق محفظتك الإلكترونية."),
|
||||
_StepTile(number: 2, text: "اختر خدمة تحويل الأموال."),
|
||||
_StepTile(
|
||||
number: 3,
|
||||
text:
|
||||
"أدخل المبلغ المطلوب: ${currencyFormat.format(widget.amount)} ل.س"),
|
||||
_StepTile(number: 4, text: "حوّل إلى الرقم التالي:"),
|
||||
// --- التعديل هنا ---
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
widget.paymentPhoneNumber,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2),
|
||||
),
|
||||
trailing: OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(text: widget.paymentPhoneNumber));
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("تم نسخ رقم الهاتف")));
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.copy, size: 18),
|
||||
label: const Text("نسخ"),
|
||||
),
|
||||
),
|
||||
// --- نهاية التعديل ---
|
||||
Image.asset(widget.qrImagePath,
|
||||
width: 180,
|
||||
height: 180,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (c, o, s) => const Icon(Icons.qr_code_2,
|
||||
size: 100, color: Colors.grey)),
|
||||
const SizedBox(height: 8),
|
||||
_StepTile(
|
||||
number: 5,
|
||||
text: "هام: انسخ رقم القسيمة والصقه في خانة \"البيان\"."),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(invoiceText,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.5)),
|
||||
trailing: OutlinedButton.icon(
|
||||
onPressed: _invoiceNumber == null
|
||||
? null
|
||||
: () async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(text: invoiceText));
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("تم نسخ رقم القسيمة")));
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.copy, size: 18),
|
||||
label: const Text("نسخ"),
|
||||
),
|
||||
),
|
||||
const Text("اضغط للتكبير",
|
||||
style: TextStyle(fontSize: 10, color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
const LinearProgressIndicator(backgroundColor: Colors.white),
|
||||
const SizedBox(height: 10),
|
||||
const Text("ننتظر إشعار الدفع تلقائياً...",
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12)),
|
||||
const SizedBox(height: 20),
|
||||
const LinearProgressIndicator(minHeight: 2),
|
||||
const SizedBox(height: 12),
|
||||
Text("بانتظار تأكيد الدفع...",
|
||||
style: TextStyle(color: Colors.grey.shade700)),
|
||||
const SizedBox(height: 4),
|
||||
const Text("هذه الشاشة ستتحدث تلقائيًا",
|
||||
style: TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -339,14 +371,21 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.check_circle, color: Colors.green, size: 80),
|
||||
const Icon(Icons.verified_rounded, color: Colors.green, size: 100),
|
||||
const SizedBox(height: 20),
|
||||
const Text("تم الدفع بنجاح!",
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text("العودة"),
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 40),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16)),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text("متابعة", style: TextStyle(fontSize: 18)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -356,47 +395,35 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error, color: Colors.red, size: 80),
|
||||
Icon(Icons.error_outline_rounded, color: Colors.red.shade400, size: 80),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
_status == PaymentStatus.paymentTimeout
|
||||
? "انتهى الوقت المحدد للدفع"
|
||||
: "حدث خطأ ما",
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text("يرجى المحاولة مرة أخرى.", style: TextStyle(fontSize: 16)),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: _createAndPollInvoice,
|
||||
child: const Text("المحاولة مرة أخرى"),
|
||||
_status == PaymentStatus.paymentTimeout
|
||||
? "انتهى الوقت"
|
||||
: "لم يتم التحقق",
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 15),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 30),
|
||||
child: Text("لم يصلنا إشعار الدفع.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey))),
|
||||
const SizedBox(height: 40),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15)),
|
||||
onPressed: _createAndPollInvoice,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text("حاول مرة أخرى"),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text("إلغاء", style: TextStyle(color: Colors.grey)))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ويدجت مساعد لعرض خطوات التعليمات بشكل أنيق
|
||||
class _StepTile extends StatelessWidget {
|
||||
final int number;
|
||||
final String text;
|
||||
const _StepTile({required this.number, required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: CircleAvatar(
|
||||
radius: 12,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
child: Text("$number",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold)),
|
||||
),
|
||||
title: Text(text),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user