feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل
قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ. الخريطة: backend · payment_server · loction_server · ride_server · passenger_server · docker · dashboard · stress_test → الجذر siro_rider → apps/rider siro_driver → apps/driver siro_admin → dashboards/admin siro_service → dashboards/service android_bot → apps/android_bot socialBot → apps/socialBot نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب) لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً: كل ما يلي يصير فرقاً مقروءاً مقابل المصدر. لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز، سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh (ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و dashboards/transit-web). ⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة: 1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر): كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner. 2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist) يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً. 3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع → يجب ضمّ الحزم داخله أسوة بـ apps/rider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9909d9b4c1
commit
4d8414c96b
@@ -0,0 +1,871 @@
|
||||
import 'package:siro_rider/constant/currency.dart';
|
||||
import 'package:siro_rider/constant/colors.dart';
|
||||
import 'package:siro_rider/constant/links.dart';
|
||||
import 'package:siro_rider/constant/style.dart';
|
||||
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:siro_rider/controller/home/map/ride_state.dart';
|
||||
import 'package:siro_rider/controller/voice_call_controller.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 '../../../constant/design.dart';
|
||||
import '../../../controller/firebase/notification_service.dart';
|
||||
import '../../../controller/functions/launch.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
class ApplyOrderWidget extends StatelessWidget {
|
||||
const ApplyOrderWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color parseColor(String colorHex) {
|
||||
if (colorHex.isEmpty) return Colors.grey;
|
||||
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 Obx(() {
|
||||
final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
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 : -400,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SiroSheetSurface(
|
||||
color: Theme.of(context).cardColor,
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.lg, AppSpacing.sm, AppSpacing.lg, AppSpacing.lg),
|
||||
child: GetBuilder<RideLifecycleController>(
|
||||
builder: (c) {
|
||||
return Column(
|
||||
mainAxisSize:
|
||||
MainAxisSize.min, // مهم جداً: يأخذ أقل مساحة ممكنة
|
||||
children: [
|
||||
// 1. دمج السعر مع الحالة في صف واحد لتوفير المساحة
|
||||
_buildCompactHeaderRow(context, c),
|
||||
|
||||
AppSpacing.vGapMd,
|
||||
|
||||
// 2. كرت المعلومات المضغوط
|
||||
_buildCompactInfoCard(context, c, parseColor),
|
||||
|
||||
AppSpacing.vGapMd,
|
||||
|
||||
// 3. أزرار الاتصال (Slim)
|
||||
_buildCompactButtonsRow(context, c),
|
||||
|
||||
AppSpacing.vGapMd,
|
||||
|
||||
// 4. شريط الوقت
|
||||
c.currentRideState.value == RideState.driverArrived
|
||||
? const DriverArrivePassengerAndWaitMinute()
|
||||
: const TimeDriverToPassenger(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// [NEW] 1. صف الرأس المضغوط (يحتوي الحالة + الإحصائيات + السعر)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildCompactHeaderRow(
|
||||
BuildContext context, RideLifecycleController controller) {
|
||||
// تنسيق السعر
|
||||
final formatter = NumberFormat("#,###");
|
||||
num totalVal = num.tryParse(controller.totalPassenger.toString()) ?? 0;
|
||||
String formattedPrice = formatter.format(totalVal);
|
||||
|
||||
// حساب الدقائق من الوقت المتبقي الحي، وليس ETA الأصلي فقط.
|
||||
final int secondsToPassenger =
|
||||
controller.remainingTimeToPassengerFromDriverAfterApplied > 0
|
||||
? controller.remainingTimeToPassengerFromDriverAfterApplied
|
||||
: controller.timeToPassengerFromDriverAfterApplied;
|
||||
final int minutes =
|
||||
secondsToPassenger <= 0 ? 0 : (secondsToPassenger / 60).ceil();
|
||||
|
||||
// تنسيق المسافة
|
||||
String distanceDisplay = "";
|
||||
try {
|
||||
double distMeters = double.parse(controller.distanceByPassenger);
|
||||
if (distMeters >= 1000) {
|
||||
distanceDisplay = "${(distMeters / 1000).toStringAsFixed(1)} km";
|
||||
} else {
|
||||
distanceDisplay = "${distMeters.toInt()} m";
|
||||
}
|
||||
} catch (e) {
|
||||
distanceDisplay = controller.distanceByPassenger;
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// القسم الأيسر: الحالة + Chips
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Driver is on the way'.tr,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13, // تصغير الخط
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
_buildMiniStatChip(
|
||||
icon: Icons.access_time_filled_rounded,
|
||||
text: minutes > 0 ? "$minutes ${'min'.tr}" : "--",
|
||||
color: AppColor.primaryColor,
|
||||
bgColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildMiniStatChip(
|
||||
icon: Icons.near_me_rounded,
|
||||
text: distanceDisplay,
|
||||
color: Colors.orange[800]!,
|
||||
bgColor: Colors.orange.withOpacity(0.1),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// القسم الأيمن: السعر (كبير وواضح في الزاوية)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
formattedPrice,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 24, // تصغير من 32 إلى 24
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColor.primaryColor,
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
CurrencyHelper.currency,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMiniStatChip({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
required Color color,
|
||||
required Color bgColor,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 12, color: color), // تصغير الأيقونة
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12, // تصغير الخط
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// [MODIFIED] 2. كرت المعلومات المضغوط جداً
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildCompactInfoCard(BuildContext context,
|
||||
RideLifecycleController controller, Color Function(String) parseColor) {
|
||||
return Container(
|
||||
// تقليل الحواف الداخلية للكرت
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: AppRadii.all(AppRadii.lg),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// الصف العلوي: سائق + سيارة
|
||||
Row(
|
||||
children: [
|
||||
// صورة السائق (أصغر)
|
||||
GestureDetector(
|
||||
onTap: () => _showDriverAvatarDialog(context, controller),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColor.primaryColor.withOpacity(0.2),
|
||||
width: 2),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 22, // تصغير من 28 إلى 22
|
||||
backgroundColor: Colors.grey[200],
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'),
|
||||
onBackgroundImageError: (_, __) =>
|
||||
const Icon(Icons.person, color: Colors.grey, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// معلومات نصية
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
controller.driverName,
|
||||
style: const TextStyle(
|
||||
fontSize: 15, // تصغير الخط
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.star_rounded,
|
||||
color: Colors.amber, size: 14),
|
||||
Text(
|
||||
" ${controller.driverRate} • ${controller.model}${controller.carColor.isNotEmpty ? ' • ${controller.carColor.tr}' : ''}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
_buildDriverBadge(
|
||||
icon: Icons.verified_rounded,
|
||||
text: controller.driverTier.tr,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
if (controller.driverCompletedRides != '0')
|
||||
_buildDriverBadge(
|
||||
icon: Icons.route_rounded,
|
||||
text:
|
||||
'${controller.driverCompletedRides} ${'rides'.tr}',
|
||||
color: Colors.teal,
|
||||
),
|
||||
if (controller.driverRatingCount != '0')
|
||||
_buildDriverBadge(
|
||||
icon: Icons.reviews_rounded,
|
||||
text:
|
||||
'${controller.driverRatingCount} ${'reviews'.tr}',
|
||||
color: Colors.amber.shade800,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// أيقونة السيارة (أصغر)
|
||||
_buildMicroCarIcon(controller, parseColor),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// لوحة السيارة (شريط نحيف جداً)
|
||||
_buildSlimLicensePlate(controller.licensePlate),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMicroCarIcon(
|
||||
RideLifecycleController controller, Color Function(String) parseColor) {
|
||||
Color carColor = parseColor(controller.colorHex);
|
||||
final String vehicleText =
|
||||
'${controller.model} ${controller.make}'.toLowerCase();
|
||||
final bool isBike = vehicleText.contains('scooter') ||
|
||||
vehicleText.contains('bike') ||
|
||||
vehicleText.contains('دراجة');
|
||||
return AnimatedCarIcon(
|
||||
child: Container(
|
||||
height: 40, // تصغير من 50
|
||||
width: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: carColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(carColor, BlendMode.srcIn),
|
||||
child: Image.asset(
|
||||
isBike ||
|
||||
box.read(BoxName.carType) == 'Scooter' ||
|
||||
box.read(BoxName.carType) == 'Pink Bike'
|
||||
? 'assets/images/moto.png'
|
||||
: 'assets/images/car3.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDriverBadge({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
required Color color,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 11, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDriverAvatarDialog(
|
||||
BuildContext context, RideLifecycleController controller) {
|
||||
final imageUrl =
|
||||
'${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg';
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 38),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 20, 18, 18),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 58,
|
||||
backgroundColor: Colors.grey[200],
|
||||
backgroundImage: NetworkImage(imageUrl),
|
||||
onBackgroundImageError: (_, __) {},
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
controller.driverName,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${controller.driverTier.tr} • ${controller.driverRate}',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
barrierDismissible: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSlimLicensePlate(String plateNumber) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Get.isDarkMode ? Colors.grey[850] : const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color:
|
||||
Get.isDarkMode ? Colors.white10 : Colors.grey.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
plateNumber,
|
||||
style: TextStyle(
|
||||
fontFamily: 'RobotoMono',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColor.writeColor,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
Text("SYR",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.writeColor.withOpacity(0.6))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// [MODIFIED] 3. أزرار الاتصال (Slim Buttons)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildCompactButtonsRow(
|
||||
BuildContext context, RideLifecycleController controller) {
|
||||
return SizedBox(
|
||||
height: AppSizes.minTouch, // ارتفاع لمس قياسي 48px
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildSlimButton(
|
||||
label: 'Message'.tr, // اختصار الكلمة
|
||||
icon: Icons.chat_bubble_outline_rounded,
|
||||
color: AppColor.blueColor,
|
||||
bgColor: AppColor.blueColor.withOpacity(0.08),
|
||||
onTap: () => _showContactOptionsDialog(context, controller),
|
||||
),
|
||||
),
|
||||
AppSpacing.gapMd,
|
||||
Expanded(
|
||||
child: _buildSlimButton(
|
||||
label: 'Call'.tr, // اختصار الكلمة
|
||||
icon: Icons.phone_rounded,
|
||||
color: Colors.white,
|
||||
bgColor: AppColor.greenColor,
|
||||
onTap: () {
|
||||
HapticFeedback.heavyImpact();
|
||||
_showCallSelectionDialog(context, controller);
|
||||
},
|
||||
isPrimary: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCallSelectionDialog(
|
||||
BuildContext context, RideLifecycleController controller) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Call Options'.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Choose how you want to call the driver'.tr,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColor.greenColor.withOpacity(0.1),
|
||||
child: Icon(Icons.phone_android_rounded,
|
||||
color: AppColor.greenColor),
|
||||
),
|
||||
title: Text('Standard Call'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text('Uses cellular network'.tr,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
makePhoneCall(controller.driverPhone);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
child: Icon(Icons.wifi_calling_3_rounded,
|
||||
color: AppColor.primaryColor),
|
||||
),
|
||||
title: Text('Free Call'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text('Voice call over internet'.tr,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
final voiceCtrl = Get.find<VoiceCallController>();
|
||||
final passengerId = box.read(BoxName.passengerID).toString();
|
||||
voiceCtrl.startCall(
|
||||
rideIdVal: controller.rideId,
|
||||
driverId: controller.driverId,
|
||||
passengerId: passengerId,
|
||||
remoteNameVal: controller.driverName,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSlimButton({
|
||||
required String label,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required Color bgColor,
|
||||
required VoidCallback onTap,
|
||||
bool isPrimary = false,
|
||||
}) {
|
||||
return ElevatedButton(
|
||||
onPressed: onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: bgColor,
|
||||
foregroundColor: color,
|
||||
elevation: isPrimary ? 2 : 0,
|
||||
padding: EdgeInsets.zero, // إزالة الحواشي الداخلية
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.md)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 18, color: color), // تصغير الأيقونة
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14, // تصغير الخط
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- النوافذ المنبثقة للرسائل (نفس الكود السابق مع تحسين بسيط) ---
|
||||
void _showContactOptionsDialog(
|
||||
BuildContext context, RideLifecycleController controller) {
|
||||
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.copyWith(fontSize: 16)),
|
||||
const SizedBox(height: 15),
|
||||
..._buildPredefinedMessages(controller),
|
||||
const Divider(height: 20),
|
||||
_buildCustomMessageInput(controller, context),
|
||||
SizedBox(height: MediaQuery.of(context).viewInsets.bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildPredefinedMessages(RideLifecycleController controller) {
|
||||
const messages = [
|
||||
'Hello, I\'m at the agreed-upon location',
|
||||
'I\'m waiting for you',
|
||||
"How much longer will you be?",
|
||||
];
|
||||
|
||||
return messages
|
||||
.map((message) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_sendMessage(controller, message.tr);
|
||||
Get.back();
|
||||
},
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.chat_bubble_outline,
|
||||
size: 16, color: AppColor.primaryColor),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(message.tr,
|
||||
style: AppStyle.subtitle.copyWith(fontSize: 13))),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Widget _buildCustomMessageInput(
|
||||
RideLifecycleController controller, BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Form(
|
||||
key: controller.messagesFormKey,
|
||||
child: TextFormField(
|
||||
controller: controller.messageToDriver,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type your message...'.tr,
|
||||
hintStyle: TextStyle(color: Colors.grey[500], fontSize: 13),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.only(bottom: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
if (controller.messagesFormKey.currentState!.validate()) {
|
||||
_sendMessage(controller, controller.messageToDriver.text);
|
||||
controller.messageToDriver.clear();
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
child: CircleAvatar(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
radius: 20,
|
||||
child:
|
||||
const Icon(Icons.send_rounded, color: Colors.white, size: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _sendMessage(RideLifecycleController controller, String text) async {
|
||||
try {
|
||||
await CRUD().post(
|
||||
link: AppLink.sendChatMessage,
|
||||
payload: {
|
||||
'ride_id': controller.rideId.toString(),
|
||||
'sender_id': box.read(BoxName.passengerID).toString(),
|
||||
'receiver_id': controller.driverId.toString(),
|
||||
'sender_type': 'passenger',
|
||||
'message_content': text.tr,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// Ignore or log error
|
||||
}
|
||||
|
||||
NotificationService.sendNotification(
|
||||
category: 'MSG_FROM_PASSENGER',
|
||||
target: controller.driverToken.toString(),
|
||||
title: text.tr,
|
||||
body: text.tr,
|
||||
isTopic: false,
|
||||
tone: 'ding',
|
||||
driverList: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// مؤشرات الانتظار والوقت (مضغوطة)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class DriverArrivePassengerAndWaitMinute extends StatelessWidget {
|
||||
const DriverArrivePassengerAndWaitMinute({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Waiting...'.tr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
color: Colors.orange)),
|
||||
Text(
|
||||
controller.stringRemainingTimeDriverWaitPassenger5Minute,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange,
|
||||
fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor: Colors.orange.withOpacity(0.2),
|
||||
color: Colors.orange,
|
||||
minHeight: 4,
|
||||
value:
|
||||
controller.progressTimerDriverWaitPassenger5Minute.toDouble(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TimeDriverToPassenger extends StatelessWidget {
|
||||
const TimeDriverToPassenger({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
if (controller.timeToPassengerFromDriverAfterApplied <= 0) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
// شريط التقدم فقط لأن الوقت والمسافة موجودان بالأعلى
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
color: AppColor.primaryColor,
|
||||
minHeight: 4,
|
||||
value: controller.progressTimerToPassengerFromDriverAfterApplied
|
||||
.toDouble()
|
||||
.clamp(0.0, 1.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedCarIcon extends StatefulWidget {
|
||||
final Widget child;
|
||||
const AnimatedCarIcon({Key? key, required this.child}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AnimatedCarIcon> createState() => _AnimatedCarIconState();
|
||||
}
|
||||
|
||||
class _AnimatedCarIconState extends State<AnimatedCarIcon>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<Offset> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 2),
|
||||
)..repeat(reverse: true);
|
||||
|
||||
_animation = Tween<Offset>(
|
||||
begin: const Offset(-0.06, 0.0),
|
||||
end: const Offset(0.06, 0.0),
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SlideTransition(
|
||||
position: _animation,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_rider/constant/colors.dart';
|
||||
import 'package:siro_rider/constant/style.dart';
|
||||
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
|
||||
// دالة لإظهار الشيت
|
||||
void showCancelRideBottomSheet() {
|
||||
Get.bottomSheet(
|
||||
const CancelRidePageWidget(),
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
|
||||
// الويدجت مفصولة لترتيب الكود
|
||||
class CancelRidePageWidget extends StatelessWidget {
|
||||
const CancelRidePageWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// تأكد من وجود الكنترولر
|
||||
final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
final List<String> reasons = [
|
||||
"Changed my mind".tr,
|
||||
"Found another transport".tr,
|
||||
"Driver is taking too long".tr,
|
||||
"Driver asked me to cancel".tr,
|
||||
"Wrong pickup location".tr,
|
||||
"Other".tr,
|
||||
];
|
||||
|
||||
return Container(
|
||||
height: Get.height * 0.7, // ارتفاع مناسب
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(25)),
|
||||
),
|
||||
child: GetBuilder<RideLifecycleController>(
|
||||
builder: (controller) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// مؤشر السحب
|
||||
Center(
|
||||
child: Container(
|
||||
width: 50,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Text(
|
||||
'Why do you want to cancel?'.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: reasons.length,
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
bool isSelected = controller.selectedReasonIndex == index;
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
reasons[index],
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color: isSelected
|
||||
? AppColor.primaryColor
|
||||
: AppColor.writeColor,
|
||||
fontSize: 15),
|
||||
),
|
||||
trailing: isSelected
|
||||
? Icon(Icons.radio_button_checked,
|
||||
color: AppColor.primaryColor)
|
||||
: Icon(Icons.radio_button_off, color: Colors.grey),
|
||||
onTap: () {
|
||||
controller.selectReason(index, reasons[index]);
|
||||
},
|
||||
),
|
||||
|
||||
// إظهار حقل النص فقط عند اختيار "أخرى"
|
||||
if (isSelected && reasons[index] == "Other".tr)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 10, left: 10, right: 10),
|
||||
child: TextField(
|
||||
controller: controller.otherReasonController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Please write the reason...".tr,
|
||||
filled: true,
|
||||
fillColor: Get.isDarkMode
|
||||
? Colors.white.withOpacity(0.05)
|
||||
: Colors.grey[100],
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 15, vertical: 12),
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// زر التأكيد
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.redColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: () => controller.cancelRide(),
|
||||
child: Text(
|
||||
'Confirm Cancellation'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// زر التراجع
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text("Don't Cancel".tr,
|
||||
style: TextStyle(color: AppColor.grayColor)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,196 @@
|
||||
import 'package:siro_rider/constant/currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_rider/constant/box_name.dart';
|
||||
import 'package:siro_rider/constant/style.dart';
|
||||
import 'package:siro_rider/views/home/my_wallet/passenger_wallet.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/design.dart';
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../../controller/payment/payment_controller.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
|
||||
class CashConfirmPageShown extends StatelessWidget {
|
||||
CashConfirmPageShown({super.key});
|
||||
final PaymentController paymentController = Get.put(PaymentController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
// شرط الإظهار الرئيسي لم يتغير
|
||||
return Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: AnimatedContainer(
|
||||
duration: AppDurations.base,
|
||||
curve: Curves.easeInOut,
|
||||
// التحكم في ظهور اللوحة لم يتغير
|
||||
transform: Matrix4.translationValues(
|
||||
0, controller.isCashConfirmPageShown ? 0 : Get.height, 0),
|
||||
child: SiroSheetSurface(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// --- 1. رأس الصفحة ---
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Payment Method'.tr,
|
||||
style: AppStyle.headTitle.copyWith(fontSize: 22),
|
||||
),
|
||||
// زر الإغلاق (كان معلقاً في الكود القديم، تم تفعيله هنا)
|
||||
IconButton(
|
||||
onPressed: () => controller.changeCashConfirmPageShown(),
|
||||
icon: Icon(Icons.close, color: AppColor.writeColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
AppSpacing.vGapLg,
|
||||
|
||||
// --- 2. بطاقات اختيار الدفع ---
|
||||
GetBuilder<PaymentController>(builder: (paymentCtrl) {
|
||||
// نفس منطق تغيير اللون للسيارات النسائية
|
||||
final bool isLadyRide = box.read(BoxName.carType) == 'Lady' ||
|
||||
box.read(BoxName.carType) == 'Pink Bike';
|
||||
final Color selectedColor =
|
||||
isLadyRide ? Colors.pink.shade300 : AppColor.primaryColor;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// بطاقة المحفظة
|
||||
_buildPaymentOptionCard(
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
title: '${AppInformation.appName} Balance'.tr,
|
||||
subtitle:
|
||||
'${'Balance:'.tr} ${box.read(BoxName.passengerWalletTotal) ?? '0.0'} ${CurrencyHelper.currency}',
|
||||
isSelected: paymentCtrl.isWalletChecked,
|
||||
selectedColor: selectedColor,
|
||||
onTap: () =>
|
||||
paymentCtrl.onChangedPaymentMethodWallet(true),
|
||||
),
|
||||
AppSpacing.vGapMd,
|
||||
// بطاقة الكاش
|
||||
_buildPaymentOptionCard(
|
||||
icon: Icons.money_rounded,
|
||||
title: 'Cash'.tr,
|
||||
subtitle: 'Pay directly to the captain'.tr,
|
||||
isSelected: paymentCtrl.isCashChecked,
|
||||
selectedColor: selectedColor,
|
||||
onTap: () =>
|
||||
paymentCtrl.onChangedPaymentMethodCash(true),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
AppSpacing.vGapXl,
|
||||
|
||||
// --- 3. أزرار التأكيد (بنفس منطقك القديم تماماً) ---
|
||||
GetBuilder<PaymentController>(builder: (paymentCtrl) {
|
||||
final bool isWalletSufficient = (double.tryParse(
|
||||
box.read(BoxName.passengerWalletTotal) ?? '0') ??
|
||||
0) >=
|
||||
double.parse(controller.totalPassenger.toString());
|
||||
|
||||
// إذا تم اختيار المحفظة والرصيد غير كافٍ
|
||||
if (paymentCtrl.isWalletChecked && !isWalletSufficient) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
title: 'Top up Balance to continue'.tr,
|
||||
onPressed: () =>
|
||||
Get.to(() => const PassengerWallet()),
|
||||
kolor: AppColor.redColor,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
paymentCtrl.onChangedPaymentMethodCash(true),
|
||||
child: Text("Or pay with Cash instead".tr,
|
||||
style: TextStyle(color: AppColor.primaryColor)),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
// في كل الحالات الأخرى (كاش، أو محفظة برصيد كافٍ)
|
||||
else {
|
||||
return MyElevatedButton(
|
||||
title: 'Confirm & Find a Ride'.tr,
|
||||
onPressed: () {
|
||||
// --- نفس منطقك القديم بالضبط ---
|
||||
controller.changeCashConfirmPageShown();
|
||||
// controller.isSearchingWindow = true;
|
||||
controller.startSearchingForDriver();
|
||||
// controller.update();
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// --- ويدجت مساعدة لبناء بطاقة الدفع ---
|
||||
Widget _buildPaymentOptionCard({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
required Color selectedColor,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: AppDurations.fast,
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
constraints: const BoxConstraints(minHeight: AppSizes.minTouch),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? selectedColor.withOpacity(0.1)
|
||||
: AppColor.writeColor.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? selectedColor
|
||||
: AppColor.writeColor.withOpacity(0.2),
|
||||
width: isSelected ? 2.0 : 1.0,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon,
|
||||
color: isSelected ? selectedColor : AppColor.writeColor,
|
||||
size: 28),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style:
|
||||
AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text(subtitle,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: AppColor.writeColor.withOpacity(0.7))),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
Icon(Icons.check_circle_rounded, color: selectedColor, size: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:siro_rider/print.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:siro_rider/env/env.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_rider/controller/home/points_for_rider_controller.dart';
|
||||
import 'package:siro_rider/services/offline_map_service.dart';
|
||||
|
||||
import '../../../controller/home/map/location_search_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../../controller/home/map/nearby_drivers_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
import '../../widgets/mydialoug.dart';
|
||||
|
||||
class GoogleMapPassengerWidget extends StatelessWidget {
|
||||
GoogleMapPassengerWidget({super.key});
|
||||
|
||||
final WayPointController wayPointController = Get.find<WayPointController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
final nearbyDrivers = Get.find<NearbyDriversController>();
|
||||
|
||||
return GetBuilder<MapEngineController>(
|
||||
builder: (controller) => rideLifecycle.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: Positioned(
|
||||
bottom: Get.height * .2,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: IntaleqMap(
|
||||
apiKey: Env.mapSaasKey,
|
||||
styleUrl: Get.isDarkMode
|
||||
? 'assets/style_dark.json'
|
||||
: 'assets/style.json',
|
||||
onMapCreated: controller.onMapCreated,
|
||||
onStyleLoaded: controller.onStyleLoaded,
|
||||
onCameraMove: locationSearch.onCameraMoveThrottled,
|
||||
onCameraIdle: () {
|
||||
if (controller.mapController != null) {
|
||||
final position = controller.mapController!.cameraPosition;
|
||||
if (position != null) {
|
||||
Log.print('✅ onCameraIdle targeted: ${position.target}');
|
||||
locationSearch
|
||||
.updateCurrentLocationFromCamera(position.target);
|
||||
OfflineMapService.instance
|
||||
.downloadRegion(position.target, radiusKm: 1.0);
|
||||
} else {
|
||||
Log.print('⚠️ onCameraIdle: cameraPosition is NULL');
|
||||
}
|
||||
} else {
|
||||
Log.print('⚠️ onCameraIdle: mapController is NULL');
|
||||
}
|
||||
},
|
||||
markers: Set.of(controller.markers),
|
||||
polylines: Set.of(controller.polyLines),
|
||||
polygons: controller.polygons,
|
||||
circles: controller.circles,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: locationSearch.passengerLocation,
|
||||
zoom: nearbyDrivers.lowPerf ? 14.5 : 15,
|
||||
),
|
||||
myLocationEnabled: true,
|
||||
onTap: (latlng) => controller.hidePlaces(),
|
||||
onLongPress: (latlng) {
|
||||
MyDialog().getDialog('Are you want to go to this site'.tr, '',
|
||||
() async {
|
||||
controller.clearPolyline();
|
||||
rideLifecycle.getDirectionMap(
|
||||
'${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}',
|
||||
'${latlng.latitude},${latlng.longitude}',
|
||||
);
|
||||
rideLifecycle.showBottomSheet1();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:siro_rider/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_rider/views/widgets/mycircular.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'dart:ui';
|
||||
import 'dart:io';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/home/map/location_search_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../../controller/home/vip_waitting_page.dart';
|
||||
import '../navigation/navigation_view.dart';
|
||||
import '../../../services/android_live_update_service.dart';
|
||||
import '../../../controller/home/ios_live_activity_service.dart';
|
||||
|
||||
// --- الدالة الرئيسية بالتصميم الجديد ---
|
||||
GetBuilder<MapEngineController> leftMainMenuIcons() {
|
||||
return GetBuilder<MapEngineController>(
|
||||
builder: (controller) {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
return Positioned(
|
||||
top: Get.height * .01,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
border: Border.all(color: AppColor.secondaryColor),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildMapActionButton(
|
||||
icon: Icons.near_me_outlined,
|
||||
tooltip: 'Toggle Map Type',
|
||||
onPressed: () => Get.to(() => NavigationView()),
|
||||
),
|
||||
_buildVerticalDivider(),
|
||||
_buildMapActionButton(
|
||||
icon: Icons.my_location_rounded,
|
||||
tooltip: 'Go to My Location',
|
||||
onPressed: () {
|
||||
controller.mapController?.animateCamera(
|
||||
CameraUpdate.newLatLng(
|
||||
LatLng(
|
||||
locationSearch.passengerLocation.latitude,
|
||||
locationSearch.passengerLocation.longitude,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildVerticalDivider(),
|
||||
_buildMapActionButton(
|
||||
icon: Octicons.watch,
|
||||
tooltip: 'VIP Waiting Page',
|
||||
onPressed: () => Get.to(() => VipWaittingPage()),
|
||||
),
|
||||
// _buildMapActionButton(
|
||||
// icon: Icons.explore,
|
||||
// tooltip: 'Test Page',
|
||||
// onPressed: () => Get.to(() => TestPage()),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMapActionButton({
|
||||
required IconData icon,
|
||||
required String tooltip,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
return IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, color: AppColor.writeColor, size: 22),
|
||||
tooltip: tooltip,
|
||||
splashRadius: 22,
|
||||
padding: const EdgeInsets.all(12),
|
||||
constraints: const BoxConstraints(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVerticalDivider() {
|
||||
return Container(
|
||||
height: 20,
|
||||
width: 1,
|
||||
color: AppColor.writeColor.withValues(alpha: 0.2),
|
||||
);
|
||||
}
|
||||
|
||||
class TestPage extends StatelessWidget {
|
||||
const TestPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Live Activity Test (iOS & Android)'),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MyCircularProgressIndicator(),
|
||||
MyElevatedButton(
|
||||
title: 'Start Live Activity Demo',
|
||||
onPressed: () {
|
||||
if (Platform.isIOS) {
|
||||
IosLiveActivityService.startRideActivity(
|
||||
rideId: "demo_trip_123",
|
||||
driverName: "أحمد علي",
|
||||
carDetails: "تويوتا كامري - أبيض",
|
||||
etaText: "5 دقائق للوصول",
|
||||
progress: 0.3,
|
||||
);
|
||||
} else if (Platform.isAndroid) {
|
||||
AndroidLiveUpdateService.startLiveUpdate(
|
||||
rideId: "demo_trip_123",
|
||||
driverName: "أحمد علي",
|
||||
carDetails: "تويوتا كامري - أبيض",
|
||||
etaText: "5 دقائق للوصول",
|
||||
progress: 0.3,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
MyElevatedButton(
|
||||
title: 'Update Live Activity (Random)',
|
||||
onPressed: () {
|
||||
final random = Random();
|
||||
final int minutes = random.nextInt(15) + 1;
|
||||
final double progress = random.nextDouble();
|
||||
final String newEtaText = "$minutes دقائق للوصول";
|
||||
|
||||
if (Platform.isIOS) {
|
||||
IosLiveActivityService.updateRideActivity(
|
||||
status: "accepted",
|
||||
driverName: "أحمد علي",
|
||||
carDetails: "تويوتا كامري - أبيض",
|
||||
etaText: newEtaText,
|
||||
progress: progress,
|
||||
);
|
||||
} else if (Platform.isAndroid) {
|
||||
AndroidLiveUpdateService.updateStatus(
|
||||
status: "accepted",
|
||||
driverName: "أحمد علي",
|
||||
carDetails: "تويوتا كامري - أبيض",
|
||||
etaText: newEtaText,
|
||||
progress: progress,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
onPressed: () async {
|
||||
if (Platform.isIOS) {
|
||||
await IosLiveActivityService.endRideActivity();
|
||||
} else if (Platform.isAndroid) {
|
||||
await AndroidLiveUpdateService.endLiveUpdate();
|
||||
}
|
||||
},
|
||||
child: const Text('End Activity'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_rider/controller/payment/payment_controller.dart';
|
||||
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
|
||||
GetBuilder<RideLifecycleController> buttomSheetMapPage() {
|
||||
Get.put(PaymentController());
|
||||
return GetBuilder<RideLifecycleController>(
|
||||
builder: (controller) =>
|
||||
controller.isBottomSheetShown && controller.rideConfirm == false
|
||||
? const Positioned(
|
||||
left: 5,
|
||||
bottom: 0,
|
||||
right: 5,
|
||||
child: Column(
|
||||
// children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.end,
|
||||
// children: [
|
||||
// double.parse(box.read(BoxName.passengerWalletTotal)) <
|
||||
// 0 &&
|
||||
// controller.data.isNotEmpty
|
||||
// ? Container(
|
||||
// decoration: AppStyle.boxDecoration
|
||||
// .copyWith(color: AppColor.redColor),
|
||||
// height: 50,
|
||||
// width: Get.width * .94,
|
||||
// child: Padding(
|
||||
// padding:
|
||||
// const EdgeInsets.symmetric(horizontal: 8),
|
||||
// child: Text(
|
||||
// 'Your trip cost is'.tr +
|
||||
// ' ${controller.totalCostPassenger.toStringAsFixed(2)} '
|
||||
// 'But you have a negative salary of'
|
||||
// .tr +
|
||||
// '${double.parse(box.read(BoxName.passengerWalletTotal)).toStringAsFixed(2)}'
|
||||
// ' in your'
|
||||
// .tr +
|
||||
// ' ${AppInformation.appName}'
|
||||
// ' wallet due to a previous trip.'
|
||||
// .tr,
|
||||
// style: AppStyle.subtitle,
|
||||
// ),
|
||||
// ))
|
||||
// : const SizedBox(),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 5,
|
||||
// ),
|
||||
// AnimatedContainer(
|
||||
// // clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
// curve: Curves.easeInCirc,
|
||||
// onEnd: () {
|
||||
// controller.height = 250;
|
||||
// },
|
||||
// height: controller.heightBottomSheetShown,
|
||||
// duration: const Duration(seconds: 2),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// controller.data.isEmpty
|
||||
// ? const SizedBox()
|
||||
// : Container(
|
||||
// // width: Get.width * .9,
|
||||
// height: 100,
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColor.secondaryColor,
|
||||
// boxShadow: [
|
||||
// const BoxShadow(
|
||||
// color: AppColor.accentColor,
|
||||
// offset: Offset(2, 2)),
|
||||
// BoxShadow(
|
||||
// color: AppColor.accentColor
|
||||
// .withOpacity(.4),
|
||||
// offset: const Offset(-2, -2))
|
||||
// ],
|
||||
// borderRadius: const BorderRadius.all(
|
||||
// Radius.circular(15))),
|
||||
// child: ListView.builder(
|
||||
// scrollDirection: Axis.horizontal,
|
||||
// itemCount: controller
|
||||
// .dataCarsLocationByPassenger.length -
|
||||
// 1,
|
||||
// itemBuilder:
|
||||
// (BuildContext context, int index) {
|
||||
// return Container(
|
||||
// color: controller.gender == 'Female'
|
||||
// ? const Color.fromARGB(
|
||||
// 255, 246, 52, 181)
|
||||
// : AppColor.secondaryColor,
|
||||
// width: Get.width,
|
||||
// child: Row(
|
||||
// mainAxisAlignment:
|
||||
// MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// SizedBox(
|
||||
// width: Get.width * .15,
|
||||
// child: Padding(
|
||||
// padding:
|
||||
// const EdgeInsets.all(8.0),
|
||||
// child: Image.asset(
|
||||
// 'assets/images/jeep.png',
|
||||
// width: 50,
|
||||
// fit: BoxFit.fill,
|
||||
// repeat: ImageRepeat.repeatX,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: Get.width * .55,
|
||||
// child: Column(
|
||||
// crossAxisAlignment:
|
||||
// CrossAxisAlignment.start,
|
||||
// mainAxisAlignment:
|
||||
// MainAxisAlignment.spaceEvenly,
|
||||
// children: [
|
||||
// Text(
|
||||
// controller.hours > 0
|
||||
// ? '${'Your Ride Duration is '.tr}${controller.hours} ${'H and'.tr} ${controller.minutes} ${'m'.tr}'
|
||||
// : '${'Your Ride Duration is '.tr} ${controller.minutes} m',
|
||||
// style: AppStyle.subtitle,
|
||||
// ),
|
||||
// // Text(
|
||||
// // '${'You will be thier in'.tr} ${DateFormat('h:mm a').format(controller.newTime)}',
|
||||
// // style: AppStyle.subtitle,
|
||||
// // ),
|
||||
// Text(
|
||||
// '${'Your trip distance is'.tr} ${controller.distance.toStringAsFixed(2)} ${'KM'.tr}',
|
||||
// style: AppStyle.subtitle,
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: Get.width * .2,
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.only(
|
||||
// right: 5, left: 5),
|
||||
// child: Column(
|
||||
// crossAxisAlignment:
|
||||
// CrossAxisAlignment.center,
|
||||
// children: [
|
||||
// Container(
|
||||
// width: Get.width * .14,
|
||||
// height: Get.height * .06,
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColor
|
||||
// .secondaryColor,
|
||||
// shape:
|
||||
// BoxShape.rectangle,
|
||||
// border: Border.all(
|
||||
// width: 2,
|
||||
// color: AppColor
|
||||
// .greenColor)),
|
||||
// child: Center(
|
||||
// child: Text(
|
||||
// '${'Fee is'.tr} \n${controller.totalPassenger.toStringAsFixed(2)}',
|
||||
// style:
|
||||
// AppStyle.subtitle,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// controller.promoTaken
|
||||
// ? const Icon(
|
||||
// Icons
|
||||
// .filter_vintage_rounded,
|
||||
// color:
|
||||
// AppColor.redColor,
|
||||
// )
|
||||
// : const SizedBox(
|
||||
// height: 0,
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 5,
|
||||
// ),
|
||||
// Container(
|
||||
// // height: 130,
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColor.secondaryColor,
|
||||
// boxShadow: [
|
||||
// const BoxShadow(
|
||||
// color: AppColor.accentColor,
|
||||
// offset: Offset(2, 2)),
|
||||
// BoxShadow(
|
||||
// color: AppColor.accentColor.withOpacity(.4),
|
||||
// offset: const Offset(-2, -2))
|
||||
// ],
|
||||
// borderRadius:
|
||||
// const BorderRadius.all(Radius.circular(15))),
|
||||
// child: controller.data.isEmpty
|
||||
// ? const SizedBox()
|
||||
// : Center(
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// horizontal: 5),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// Row(
|
||||
// children: [
|
||||
// const Icon(
|
||||
// Icons.location_on,
|
||||
// color: AppColor.redColor,
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// width: 10,
|
||||
// ),
|
||||
// Text(
|
||||
// 'From : '.tr,
|
||||
// style: AppStyle.subtitle,
|
||||
// ),
|
||||
// Text(
|
||||
// controller.data[0]
|
||||
// ['start_address']
|
||||
// .toString(),
|
||||
// style: AppStyle.subtitle,
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// Row(
|
||||
// children: [
|
||||
// const Icon(Icons
|
||||
// .location_searching_rounded),
|
||||
// const SizedBox(
|
||||
// width: 10,
|
||||
// ),
|
||||
// Text(
|
||||
// 'To : '.tr,
|
||||
// style: AppStyle.subtitle,
|
||||
// ),
|
||||
// Text(
|
||||
// controller.data[0]['end_address'],
|
||||
// style: AppStyle.subtitle,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// const Divider(
|
||||
// color: AppColor.accentColor,
|
||||
// thickness: 1,
|
||||
// height: 2,
|
||||
// indent: 1,
|
||||
// ),
|
||||
// SizedBox(
|
||||
// height: 40,
|
||||
// child: Row(
|
||||
// mainAxisAlignment:
|
||||
// MainAxisAlignment.center,
|
||||
// children: [
|
||||
// Container(
|
||||
// decoration: BoxDecoration(
|
||||
// color:
|
||||
// AppColor.secondaryColor,
|
||||
// borderRadius:
|
||||
// BorderRadius.circular(12),
|
||||
// // border: Border.all(),
|
||||
// ),
|
||||
// child: Row(
|
||||
// children: [
|
||||
// Icon(
|
||||
// Icons.monetization_on,
|
||||
// color: Colors.green[400],
|
||||
// ),
|
||||
// InkWell(
|
||||
// onTap: () async {
|
||||
// controller
|
||||
// .changeCashConfirmPageShown();
|
||||
// Get.find<
|
||||
// PaymentController>()
|
||||
// .getPassengerWallet();
|
||||
// },
|
||||
// child: GetBuilder<
|
||||
// PaymentController>(
|
||||
// builder: (paymentController) =>
|
||||
// paymentController
|
||||
// .isCashChecked
|
||||
// ? Text(
|
||||
// 'CASH',
|
||||
// style: AppStyle
|
||||
// .title,
|
||||
// )
|
||||
// : Text(
|
||||
// '${AppInformation.appName} Wallet',
|
||||
// style: AppStyle
|
||||
// .title,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// width: 40,
|
||||
// ),
|
||||
// GetBuilder<PaymentController>(
|
||||
// builder:
|
||||
// (paymentController) =>
|
||||
// Container(
|
||||
// decoration:
|
||||
// BoxDecoration(
|
||||
// color: AppColor
|
||||
// .secondaryColor,
|
||||
// borderRadius:
|
||||
// BorderRadius
|
||||
// .circular(
|
||||
// 12),
|
||||
// ),
|
||||
// child: Row(
|
||||
// children: [
|
||||
// Icon(
|
||||
// Icons
|
||||
// .qr_code_2_rounded,
|
||||
// color: Colors
|
||||
// .green[
|
||||
// 400],
|
||||
// ),
|
||||
// InkWell(
|
||||
// onTap: () {
|
||||
// if (controller
|
||||
// .promoTaken ==
|
||||
// false) {
|
||||
// Get.defaultDialog(
|
||||
// title: 'Add Promo'.tr,
|
||||
// titleStyle: AppStyle.title,
|
||||
// content: Column(
|
||||
// children: [
|
||||
// SizedBox(
|
||||
// width: Get.width * .7,
|
||||
// child: TextFormField(
|
||||
// controller: controller.promo,
|
||||
// decoration: InputDecoration(
|
||||
// labelText: 'Promo Code'.tr,
|
||||
// hintText: 'Enter promo code'.tr,
|
||||
// labelStyle: AppStyle.subtitle,
|
||||
// hintStyle: AppStyle.subtitle,
|
||||
// border: OutlineInputBorder(
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// ),
|
||||
// filled: true,
|
||||
// fillColor: Colors.grey[200],
|
||||
// focusedBorder: OutlineInputBorder(
|
||||
// borderSide: const BorderSide(
|
||||
// color: AppColor.primaryColor,
|
||||
// width: 2.0,
|
||||
// ),
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// ),
|
||||
// errorBorder: OutlineInputBorder(
|
||||
// borderSide: const BorderSide(
|
||||
// color: Colors.red,
|
||||
// width: 2.0,
|
||||
// ),
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// ),
|
||||
// enabledBorder: OutlineInputBorder(
|
||||
// borderSide: const BorderSide(
|
||||
// color: Colors.grey,
|
||||
// width: 1.0,
|
||||
// ),
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// MyElevatedButton(
|
||||
// title: 'Add Promo'.tr,
|
||||
// onPressed: () async {
|
||||
// controller.applyPromoCodeToPassenger();
|
||||
// },
|
||||
// )
|
||||
// ],
|
||||
// ));
|
||||
// } else {
|
||||
// Get.snackbar(
|
||||
// 'You have promo!'
|
||||
// .tr,
|
||||
// '',
|
||||
// backgroundColor:
|
||||
// AppColor.redColor);
|
||||
// }
|
||||
// },
|
||||
// child: Text(
|
||||
// 'Add Promo'
|
||||
// .tr,
|
||||
// style: AppStyle
|
||||
// .title,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// )),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: Get.width * .95,
|
||||
// child: Row(
|
||||
// mainAxisAlignment:
|
||||
// MainAxisAlignment.center,
|
||||
// children: [
|
||||
// controller.isCashSelectedBeforeConfirmRide ==
|
||||
// false
|
||||
// ? MyElevatedButton(
|
||||
// title: 'Next'.tr,
|
||||
// onPressed: () {
|
||||
// controller
|
||||
// .changeCashConfirmPageShown();
|
||||
// },
|
||||
// )
|
||||
// :
|
||||
// // controller.isPassengerChosen ==
|
||||
// // false
|
||||
// // ? MyElevatedButton(
|
||||
// // title: 'Next'.tr,
|
||||
// // onPressed: () {
|
||||
// // controller
|
||||
// // .onChangedPassengersChoose();
|
||||
// // Get.defaultDialog(
|
||||
// // barrierDismissible:
|
||||
// // false,
|
||||
// // title:
|
||||
// // 'How Many Passengers?'
|
||||
// // .tr,
|
||||
// // titleStyle:
|
||||
// // AppStyle
|
||||
// // .title,
|
||||
// // content:
|
||||
// // Column(
|
||||
// // children: [
|
||||
// // Text(
|
||||
// // 'Allowed up to 4 Passengers.'
|
||||
// // .tr,
|
||||
// // style: AppStyle
|
||||
// // .title,
|
||||
// // ),
|
||||
// // SizedBox(
|
||||
// // height:
|
||||
// // 200, // Set the desired height here
|
||||
// // child:
|
||||
// // CupertinoPicker(
|
||||
// // itemExtent:
|
||||
// // 32,
|
||||
// // onSelectedItemChanged:
|
||||
// // (index) {
|
||||
// // controller.onChangedPassengerCount(index +
|
||||
// // 1);
|
||||
// // },
|
||||
// // children: [
|
||||
// // Text('1 Passenger'.tr),
|
||||
// // Text('2 Passengers'.tr),
|
||||
// // Text('3 Passengers'.tr),
|
||||
// // Text('4 Passengers'.tr),
|
||||
// // ],
|
||||
// // ),
|
||||
// // ),
|
||||
// // MyElevatedButton(
|
||||
// // title:
|
||||
// // 'Back',
|
||||
// // onPressed:
|
||||
// // () =>
|
||||
// // Get.back(),
|
||||
// // )
|
||||
// // ],
|
||||
// // ),
|
||||
// // );
|
||||
// // },
|
||||
// // )
|
||||
// // :
|
||||
// MyElevatedButton(
|
||||
// title: 'Confirm Selection'
|
||||
// .tr,
|
||||
// onPressed: () {
|
||||
// controller
|
||||
// .confirmRideForFirstDriver();
|
||||
// },
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
),
|
||||
)
|
||||
: const SizedBox());
|
||||
}
|
||||
|
||||
class Details extends StatelessWidget {
|
||||
const Details({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<RideLifecycleController>(
|
||||
builder: (controller) => Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text(
|
||||
'${'Distance is'.tr} ${controller.distance.toStringAsFixed(2)} KM',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Duration is'.tr} ${controller.data[0]['duration']['text']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text(
|
||||
'Cost for .21/km ${controller.costDistance.toStringAsFixed(2)} ',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Cost Duration'.tr} ${controller.averageDuration.toStringAsFixed(2)} is ${controller.costDuration.toStringAsFixed(2)} ',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text(
|
||||
'Total Driver ${controller.totalDriver.toStringAsFixed(2)}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'totaME ${controller.totalME.toStringAsFixed(2)} ',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'Cost for passenger ${double.parse(controller.totalPassenger.toString()).toStringAsFixed(2)} ',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:siro_rider/constant/box_name.dart';
|
||||
import 'package:siro_rider/main.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_rider/constant/style.dart';
|
||||
import 'package:siro_rider/views/home/my_wallet/passenger_wallet.dart';
|
||||
import 'package:siro_rider/views/home/profile/complaint_page.dart';
|
||||
import 'package:siro_rider/views/home/profile/order_history.dart';
|
||||
import 'package:siro_rider/views/home/profile/promos_passenger_page.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../widgets/error_snakbar.dart';
|
||||
import '../../notification/notification_page.dart';
|
||||
import '../HomePage/contact_us.dart';
|
||||
import '../HomePage/share_app_page.dart';
|
||||
import '../setting_page.dart';
|
||||
import '../profile/passenger_profile_page.dart';
|
||||
import '../../transit/transit_home_page.dart';
|
||||
|
||||
// ─── ألوان النظام (Integrated with AppColor) ──────────────────────────────────
|
||||
Color get _kCyan => AppColor.cyanBlue;
|
||||
Color get _kBg =>
|
||||
Get.isDarkMode ? const Color(0xFF060B18) : AppColor.secondaryColor;
|
||||
Color get _kBgSurface => Get.isDarkMode
|
||||
? const Color(0xFF0D1525)
|
||||
: AppColor.secondaryColor.withValues(alpha: 0.9);
|
||||
const _kAmber = Color(0xFFFFB700);
|
||||
Color get _kText => AppColor.writeColor;
|
||||
Color get _kTextMuted => AppColor.grayColor;
|
||||
|
||||
class MapMenuWidget extends StatelessWidget {
|
||||
const MapMenuWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapEngineController>(
|
||||
builder: (controller) => Stack(
|
||||
children: [
|
||||
// ── تعتيم الخلفية ───────────────────────────────────────────────
|
||||
if (controller.widthMenu > 0)
|
||||
GestureDetector(
|
||||
onTap: controller.getDrawerMenu,
|
||||
child: Container(color: Colors.black.withValues(alpha: 0.55)),
|
||||
),
|
||||
|
||||
_buildSideMenu(controller),
|
||||
_buildMenuButton(controller),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── زر القائمة العائم ────────────────────────────────────────────────────
|
||||
Widget _buildMenuButton(MapEngineController controller) {
|
||||
return Positioned(
|
||||
top: 45,
|
||||
left: 16,
|
||||
child: SafeArea(
|
||||
child: GestureDetector(
|
||||
onTap: controller.getDrawerMenu,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: _kBg.withValues(alpha: 0.88),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: _kCyan.withValues(alpha: 0.25), width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: _kCyan.withValues(alpha: 0.12),
|
||||
blurRadius: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Icon(
|
||||
controller.widthMenu > 0
|
||||
? Icons.close_rounded
|
||||
: Icons.menu_rounded,
|
||||
key: ValueKey(controller.widthMenu > 0),
|
||||
color: _kCyan,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── القائمة الجانبية ─────────────────────────────────────────────────────
|
||||
Widget _buildSideMenu(MapEngineController controller) {
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 420),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: controller.widthMenu > 0 ? 0 : -Get.width,
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Container(
|
||||
width: Get.width * 0.8,
|
||||
constraints: const BoxConstraints(maxWidth: 320),
|
||||
decoration: BoxDecoration(
|
||||
color: _kBg.withValues(alpha: 0.97),
|
||||
border: Border(
|
||||
right:
|
||||
BorderSide(color: _kCyan.withValues(alpha: 0.12), width: 1),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
blurRadius: 32,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// شبكة خلفية
|
||||
Positioned.fill(
|
||||
child: CustomPaint(painter: _MenuGridPainter())),
|
||||
|
||||
// المحتوى
|
||||
SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildMenuHeader(),
|
||||
_buildQuickActionButtons(),
|
||||
_buildDivider(),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 4),
|
||||
children: [
|
||||
MenuListItem(
|
||||
title: 'Mawasalati'.tr,
|
||||
icon: Icons.directions_bus_filled_rounded,
|
||||
onTap: () =>
|
||||
Get.to(() => const TransitHomePage()),
|
||||
),
|
||||
MenuListItem(
|
||||
title: 'My Balance'.tr,
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
onTap: () =>
|
||||
Get.to(() => const PassengerWallet()),
|
||||
),
|
||||
MenuListItem(
|
||||
title: 'Order History'.tr,
|
||||
icon: Icons.history_rounded,
|
||||
onTap: () => Get.to(() => const OrderHistory()),
|
||||
),
|
||||
MenuListItem(
|
||||
title: 'Promos'.tr,
|
||||
icon: Icons.local_offer_outlined,
|
||||
onTap: () =>
|
||||
Get.to(() => const PromosPassengerPage()),
|
||||
),
|
||||
MenuListItem(
|
||||
title: 'Contact Us'.tr,
|
||||
icon: Icons.contact_support_outlined,
|
||||
onTap: () => Get.to(() => ContactUsPage()),
|
||||
),
|
||||
MenuListItem(
|
||||
title: 'Complaint'.tr,
|
||||
icon: Icons.flag_outlined,
|
||||
onTap: () => Get.to(() => ComplaintPage()),
|
||||
),
|
||||
MenuListItem(
|
||||
title: 'Driver'.tr,
|
||||
icon: Ionicons.car_sport_outline,
|
||||
onTap: () => _launchDriverAppUrl(),
|
||||
),
|
||||
MenuListItem(
|
||||
title: 'Share App'.tr,
|
||||
icon: Icons.share_outlined,
|
||||
onTap: () => Get.to(() => ShareAppPage()),
|
||||
),
|
||||
MenuListItem(
|
||||
title: 'Privacy Policy'.tr,
|
||||
icon: Icons.shield_outlined,
|
||||
onTap: () => launchUrl(
|
||||
Uri.parse(
|
||||
'${AppLink.server}/privacy_policy.php'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildDivider(),
|
||||
// زر الخروج
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
|
||||
child: MenuListItem(
|
||||
title: 'Logout'.tr,
|
||||
icon: Icons.logout_rounded,
|
||||
isDestructive: true,
|
||||
onTap: () {
|
||||
Get.defaultDialog(
|
||||
title: "Logout".tr,
|
||||
middleText: "Are you sure you want to logout?".tr,
|
||||
textConfirm: "Logout".tr,
|
||||
textCancel: "Cancel".tr,
|
||||
onConfirm: () {
|
||||
// controller.logout();
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── رأس القائمة ──────────────────────────────────────────────────────────
|
||||
Widget _buildMenuHeader() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 24, 16, 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: _kBgSurface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: _kCyan.withValues(alpha: 0.15), width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// أفاتار المستخدم
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
_kCyan.withValues(alpha: 0.2),
|
||||
_kAmber.withValues(alpha: 0.12),
|
||||
],
|
||||
),
|
||||
border: Border.all(
|
||||
color: _kCyan.withValues(alpha: 0.35), width: 1.5),
|
||||
),
|
||||
child: Icon(Icons.person_rounded, color: _kCyan, size: 28),
|
||||
),
|
||||
// نقطة الحضور
|
||||
Positioned(
|
||||
bottom: 1,
|
||||
right: 1,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00E676),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: _kBg, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF00E676).withValues(alpha: 0.5),
|
||||
blurRadius: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
box.read(BoxName.name) ?? 'Guest',
|
||||
style: TextStyle(
|
||||
color: _kText,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 5,
|
||||
height: 5,
|
||||
decoration:
|
||||
BoxDecoration(color: _kCyan, shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"Siro Passenger".tr,
|
||||
style: TextStyle(
|
||||
color: _kTextMuted,
|
||||
fontSize: 12,
|
||||
letterSpacing: 0.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── أزرار الإجراءات السريعة ───────────────────────────────────────────────
|
||||
Widget _buildQuickActionButtons() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
_QuickBtn(
|
||||
icon: Icons.person_outline_rounded,
|
||||
label: 'Profile'.tr,
|
||||
onTap: () => Get.to(() => PassengerProfilePage()),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_QuickBtn(
|
||||
icon: Icons.notifications_none_rounded,
|
||||
label: 'Alerts'.tr,
|
||||
onTap: () => Get.to(() => const NotificationPage()),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_QuickBtn(
|
||||
icon: Icons.settings_outlined,
|
||||
label: 'Settings'.tr,
|
||||
onTap: () => Get.to(() => const SettingPage()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDivider() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
height: 1,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
_kCyan.withValues(alpha: 0.15),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _launchDriverAppUrl() async {
|
||||
final String driverAppUrl;
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
driverAppUrl =
|
||||
'https://play.google.com/store/apps/details?id=com.siro_driver';
|
||||
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
|
||||
driverAppUrl = 'https://apps.apple.com/st/app/siro-driver/id6482995159';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final Uri url = Uri.parse(driverAppUrl);
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url);
|
||||
} else {
|
||||
mySnackbarError('Could not launch driver app store.');
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackbarError('Could not open the link.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── زر الإجراء السريع ────────────────────────────────────────────────────────
|
||||
class _QuickBtn extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _QuickBtn({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: _kBgSurface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: _kCyan.withValues(alpha: 0.12), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: _kCyan, size: 22),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: _kTextMuted,
|
||||
fontSize: 11,
|
||||
letterSpacing: 0.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── عنصر القائمة ─────────────────────────────────────────────────────────────
|
||||
class MenuListItem extends StatelessWidget {
|
||||
const MenuListItem({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
required this.icon,
|
||||
this.color,
|
||||
this.isDestructive = false,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
final Color? color;
|
||||
final bool isDestructive;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconColor = isDestructive
|
||||
? const Color(0xFFFF5252)
|
||||
: (color ?? _kCyan.withValues(alpha: 0.80));
|
||||
final textColor =
|
||||
isDestructive ? const Color(0xFFFF5252) : (color ?? _kText);
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
splashColor: _kCyan.withValues(alpha: 0.07),
|
||||
highlightColor: _kCyan.withValues(alpha: 0.04),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
// أيقونة بخلفية دقيقة
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: isDestructive
|
||||
? const Color(0xFFFF5252).withValues(alpha: 0.08)
|
||||
: _kCyan.withValues(alpha: 0.07),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, size: 19, color: iconColor),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title.tr,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: _kTextMuted.withValues(alpha: 0.4),
|
||||
size: 18,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── رسّام الشبكة ──────────────────────────────────────────────────────────────
|
||||
class _MenuGridPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = AppColor.cyanBlue.withValues(alpha: 0.04)
|
||||
..strokeWidth = 0.5;
|
||||
const spacing = 36.0;
|
||||
for (double y = 0; y < size.height; y += spacing) {
|
||||
canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
|
||||
}
|
||||
for (double x = 0; x < size.width; x += spacing) {
|
||||
canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_MenuGridPainter old) => false;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:ui'; // مهم لإضافة تأثير الضبابية
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map/location_search_controller.dart';
|
||||
|
||||
// --- الويدجت الرئيسية بالتصميم الجديد ---
|
||||
class PassengerRideLocationWidget extends StatefulWidget {
|
||||
const PassengerRideLocationWidget({super.key});
|
||||
|
||||
@override
|
||||
State<PassengerRideLocationWidget> createState() =>
|
||||
_PassengerRideLocationWidgetState();
|
||||
}
|
||||
|
||||
class _PassengerRideLocationWidgetState
|
||||
extends State<PassengerRideLocationWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// --- إعداد الأنيميشن للأيقونة ---
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
)..repeat(reverse: true); // التكرار بشكل عكسي لإنشاء تأثير النبض
|
||||
|
||||
_animation = Tween<double>(begin: 0.9, end: 1.1).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<LocationSearchController>(builder: (controller) {
|
||||
// --- نفس شرط الإظهار الخاص بك ---
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
bottom: controller.isPassengerRideLocationWidget
|
||||
? 20
|
||||
: -100, // حركة دخول وخروج ناعمة
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50.0), // حواف دائرية بالكامل
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), // تأثير زجاجي
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor.withValues(alpha: 0.85),
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
border: Border.all(color: AppColor.writeColor.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// --- أيقونة متحركة لجذب الانتباه ---
|
||||
ScaleTransition(
|
||||
scale: _animation,
|
||||
child: Icon(
|
||||
Icons.location_on,
|
||||
color: AppColor.primaryColor,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// --- نص إرشادي واضح ---
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Set pickup location".tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
"Move the map to adjust the pin".tr,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: AppColor.writeColor.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_rider/constant/box_name.dart';
|
||||
import 'package:siro_rider/controller/functions/secure_storage.dart';
|
||||
import 'package:siro_rider/controller/home/payment/credit_card_controller.dart';
|
||||
import 'package:siro_rider/views/widgets/elevated_btn.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
|
||||
class PaymentMethodPage extends StatelessWidget {
|
||||
const PaymentMethodPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapEngineController>(
|
||||
builder: (controller) => Positioned(
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
left: 5,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
height: controller.isPaymentMethodPageShown
|
||||
? controller.paymentPageShown
|
||||
: 0,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'My Card'.tr,
|
||||
style: AppStyle.title.copyWith(fontSize: 22),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
controller.changePaymentMethodPageShown(),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Add Card'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
// GetBuilder<CreditCardController>(
|
||||
// builder: (controller) => IconButton(
|
||||
// onPressed: () {
|
||||
// // controller.scanCard();
|
||||
// // Get.defaultDialog(content: OptionConfigureWidget(
|
||||
// // initialOptions: scanOptions,
|
||||
// // onScanOptionChanged: (newOptions) =>
|
||||
// // scanOptions = newOptions,
|
||||
// // ),
|
||||
// // );
|
||||
// },
|
||||
// icon: const Icon(Icons.contact_emergency_sharp),
|
||||
// ),
|
||||
// )
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const MyCreditCardWidget(),
|
||||
const Spacer(),
|
||||
GetBuilder<CreditCardController>(
|
||||
builder: (controller) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
title: 'Add Credit Card'.tr,
|
||||
onPressed: () async {
|
||||
if (controller.formKey.currentState!
|
||||
.validate()) {
|
||||
SecureStorage().saveData(
|
||||
BoxName.cardNumber,
|
||||
controller
|
||||
.cardNumberController.text);
|
||||
SecureStorage().saveData(
|
||||
BoxName.cardHolderName,
|
||||
controller
|
||||
.cardHolderNameController.text);
|
||||
SecureStorage().saveData(
|
||||
BoxName.expiryDate,
|
||||
controller
|
||||
.expiryDateController.text);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class MyCreditCardWidget extends StatelessWidget {
|
||||
const MyCreditCardWidget({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(CreditCardController());
|
||||
return GetBuilder<CreditCardController>(
|
||||
builder: (controller) => Container(
|
||||
height: Get.height * .4,
|
||||
width: Get.width * .9,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
gradient: LinearGradient(colors: [
|
||||
AppColor.secondaryColor,
|
||||
// AppColor.blueColor,
|
||||
// AppColor.greenColor,
|
||||
AppColor.accentColor,
|
||||
// AppColor.primaryColor,
|
||||
// AppColor.redColor,
|
||||
// AppColor.yellowColor
|
||||
]),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
spreadRadius: 3,
|
||||
offset: Offset(3, 3),
|
||||
blurRadius: 3,
|
||||
color: AppColor.redColor),
|
||||
BoxShadow(
|
||||
offset: Offset(-3, -3),
|
||||
blurRadius: 3,
|
||||
spreadRadius: 3,
|
||||
color: AppColor.redColor),
|
||||
],
|
||||
),
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: Row(
|
||||
children: [
|
||||
getCardIcon(controller.cardNumberController
|
||||
.text), // Dynamic credit card icon
|
||||
SizedBox(
|
||||
width: Get.width * .03,
|
||||
),
|
||||
SizedBox(
|
||||
width: Get.width * .25,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
'Card Number'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: Get.width * .03,
|
||||
),
|
||||
SizedBox(
|
||||
width: Get.width * .4,
|
||||
height: 70,
|
||||
child: TextFormField(
|
||||
maxLength: 16,
|
||||
keyboardType: TextInputType.number,
|
||||
controller: controller.cardNumberController,
|
||||
style: const TextStyle(
|
||||
color: AppColor.blueColor,
|
||||
fontFamily: 'digital-counter-7',
|
||||
fontWeight: FontWeight.bold),
|
||||
decoration: const InputDecoration(
|
||||
helperStyle: TextStyle(
|
||||
fontFamily: 'digital-counter-7'),
|
||||
// labelText: 'Card Number',
|
||||
),
|
||||
// inputFormatters: [DigitObscuringFormatter()],
|
||||
validator: (value) {
|
||||
if (value!.isEmpty || value.length != 16) {
|
||||
return 'Please enter a valid 16-digit card number'
|
||||
.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.person),
|
||||
SizedBox(
|
||||
width: Get.width * .03,
|
||||
),
|
||||
SizedBox(
|
||||
width: Get.width * .25,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
'Holder Name'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: Get.width * .03,
|
||||
),
|
||||
SizedBox(
|
||||
width: Get.width * .3,
|
||||
child: SizedBox(
|
||||
// height: 50,
|
||||
child: TextFormField(
|
||||
style: AppStyle.title,
|
||||
keyboardType: TextInputType.text,
|
||||
// maxLength: 16,
|
||||
controller: controller.cardHolderNameController,
|
||||
decoration: const InputDecoration(
|
||||
// labelText: 'Cardholder Name',
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) {
|
||||
return 'Please enter the cardholder name'
|
||||
.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: Get.width * .4,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.date_range_outlined),
|
||||
SizedBox(
|
||||
width: Get.width * .03,
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: Get.width * .2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
'Expiry Date'.tr,
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: Get.width * .1,
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
child: TextFormField(
|
||||
maxLength: 4,
|
||||
keyboardType: TextInputType.datetime,
|
||||
controller:
|
||||
controller.expiryDateController,
|
||||
style: AppStyle.title,
|
||||
decoration: const InputDecoration(),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) {
|
||||
return 'Please enter the expiry date'
|
||||
.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: Get.width * .4,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.security),
|
||||
SizedBox(
|
||||
width: Get.width * .021,
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: Get.width * .2,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
'CVV Code'.tr,
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: Get.width * .2,
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
child: TextFormField(
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.number,
|
||||
style: const TextStyle(
|
||||
color: AppColor.primaryColor,
|
||||
fontFamily: 'digital-counter-7'),
|
||||
maxLength: 3,
|
||||
controller:
|
||||
controller.cvvCodeController,
|
||||
decoration: const InputDecoration(
|
||||
// labelText: 'CVV Code',
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty &&
|
||||
value.length != 3) {
|
||||
return 'Please enter the CVV code'
|
||||
.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// const SizedBox(
|
||||
// height: 20,
|
||||
// ),
|
||||
MyElevatedButton(
|
||||
title: 'Save'.tr,
|
||||
onPressed: () {
|
||||
if (controller.formKey.currentState!.validate()) {
|
||||
// final creditCard = CreditCardModel(
|
||||
// cardNumber: controller.cardNumberController.text,
|
||||
// cardHolderName:
|
||||
// controller.cardHolderNameController.text,
|
||||
// expiryDate: controller.expiryDateController.text,
|
||||
// cvvCode: controller.cvvCodeController.text,
|
||||
// );
|
||||
// Process the credit card details
|
||||
// You can use GetX to handle the logic here
|
||||
|
||||
if (controller.formKey.currentState!.validate()) {
|
||||
SecureStorage().saveData(BoxName.cardNumber,
|
||||
controller.cardNumberController.text);
|
||||
SecureStorage().saveData(BoxName.cardHolderName,
|
||||
controller.cardHolderNameController.text);
|
||||
SecureStorage().saveData(BoxName.expiryDate,
|
||||
controller.expiryDateController.text);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
))));
|
||||
}
|
||||
|
||||
Widget getCardIcon(String cardNumber) {
|
||||
String cardType = detectCardType(
|
||||
cardNumber); // Function to detect card type based on the first digit
|
||||
|
||||
IconData iconData;
|
||||
Color iconColor;
|
||||
|
||||
switch (cardType) {
|
||||
case 'Visa':
|
||||
iconData = Icons.credit_card_rounded;
|
||||
iconColor = Colors.blue; // Change color for Visa cards
|
||||
break;
|
||||
case 'Mastercard':
|
||||
iconData = Icons.credit_card_rounded;
|
||||
iconColor = Colors.red; // Change color for Mastercard cards
|
||||
break;
|
||||
default:
|
||||
iconData = Icons.credit_card_rounded;
|
||||
iconColor = Colors.black; // Default color for other card types
|
||||
break;
|
||||
}
|
||||
|
||||
return Icon(
|
||||
iconData,
|
||||
color: iconColor,
|
||||
);
|
||||
}
|
||||
|
||||
String detectCardType(String cardNumber) {
|
||||
if (cardNumber.startsWith('4')) {
|
||||
return 'Visa';
|
||||
} else if (cardNumber.startsWith('5')) {
|
||||
return 'Mastercard';
|
||||
} else {
|
||||
return 'Other';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_rider/constant/style.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/home/map/location_search_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../../controller/home/points_for_rider_controller.dart';
|
||||
|
||||
class PointsPageForRider extends StatelessWidget {
|
||||
PointsPageForRider({
|
||||
super.key,
|
||||
});
|
||||
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.find<WayPointController>();
|
||||
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
return Positioned(
|
||||
bottom: 2,
|
||||
left: 2,
|
||||
right: 2,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
height: controller.wayPointSheetHeight,
|
||||
decoration: AppStyle.boxDecoration,
|
||||
child: ListView(
|
||||
children: [
|
||||
// const AppBarPointsPageForRider(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
mapEngine.downPoints();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_drop_down_circle_outlined),
|
||||
),
|
||||
GetBuilder<WayPointController>(builder: (wayPointController) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
wayPointController.addWayPoints();
|
||||
controller.isWayPointStopsSheetUtilGetMap = true;
|
||||
},
|
||||
child: const Text('Add Stops'),
|
||||
),
|
||||
wayPointController.wayPoints.length > 1
|
||||
? ElevatedButton(
|
||||
onPressed: () async {
|
||||
locationSearch
|
||||
.getMapPointsForAllMethods();
|
||||
},
|
||||
child: const Text('Get Direction'),
|
||||
)
|
||||
: const SizedBox()
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: Get.height * .36,
|
||||
child: GetBuilder<WayPointController>(
|
||||
builder: (wayPointController) {
|
||||
return ReorderableListView(
|
||||
// The children of the list are the text fields
|
||||
children: wayPointController.wayPoints
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) {
|
||||
final index = entry.key;
|
||||
return Padding(
|
||||
key: ValueKey(index),
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.deepPurpleAccent,
|
||||
border: Border.all(),
|
||||
shape: BoxShape.rectangle),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: Text(
|
||||
index.toString(),
|
||||
style: AppStyle.title,
|
||||
),
|
||||
)),
|
||||
title: InkWell(
|
||||
onTap: () {
|
||||
// showAddLocationDialog(context);
|
||||
Get.defaultDialog(
|
||||
content: SizedBox(
|
||||
width: Get.width,
|
||||
height: 400,
|
||||
child: locationSearch
|
||||
.placeListResponse[index]),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(),
|
||||
color:
|
||||
AppColor.accentColor.withValues(alpha: 0.5)),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(index > 0
|
||||
? locationSearch
|
||||
.currentLocationStringAll[index]
|
||||
.toString()
|
||||
: ''),
|
||||
const Icon(
|
||||
Icons.reorder,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: index > 0
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
wayPointController.removeTextField(index);
|
||||
},
|
||||
)
|
||||
: IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: AppColor.secondaryColor,
|
||||
),
|
||||
onPressed: () {},
|
||||
)),
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
// The callback when the user reorders the text fields
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
wayPointController.reorderTextFields(oldIndex, newIndex);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// GetBuilder<PointsForRiderController>(
|
||||
// builder: (controller) => Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// height: Get.height *
|
||||
// .5, // height: controller.heightPointsPageForRider,
|
||||
// width: Get.width,
|
||||
// child: Column(
|
||||
// children: [
|
||||
// SizedBox(
|
||||
// height: 300,
|
||||
// child: ReorderableListView(
|
||||
// onReorder: (oldIndex, newIndex) {
|
||||
// if (oldIndex < newIndex) {
|
||||
// newIndex -= 1;
|
||||
// }
|
||||
// pointsForRiderController.locations.insert(
|
||||
// newIndex,
|
||||
// pointsForRiderController.locations
|
||||
// .removeAt(oldIndex));
|
||||
// },
|
||||
// children: [
|
||||
// for (int i = 0;
|
||||
// i < pointsForRiderController.locations.length;
|
||||
// i++)
|
||||
// ListTile(
|
||||
// key: Key('$i'),
|
||||
// title: DragTarget<int>(
|
||||
// onAccept: (int data) {
|
||||
// pointsForRiderController.locations
|
||||
// .insert(i, 'New Text Field');
|
||||
// },
|
||||
// builder: (context, candidateData, rejectedData) {
|
||||
// return Row(
|
||||
// children: [
|
||||
// SizedBox(
|
||||
// width: 300,
|
||||
// child: TextField(
|
||||
// controller: TextEditingController(
|
||||
// text: pointsForRiderController
|
||||
// .locations[i]),
|
||||
// onChanged: (value) {
|
||||
// pointsForRiderController
|
||||
// .locations[i] = value;
|
||||
// },
|
||||
// decoration: InputDecoration(
|
||||
// prefixIcon: IconButton(
|
||||
// onPressed: () {
|
||||
// pointsForRiderController
|
||||
// .removeLocation(i);
|
||||
// },
|
||||
// icon: const Icon(Icons.delete),
|
||||
// ),
|
||||
// labelText: 'Text Field ${i + 1}',
|
||||
// border: const OutlineInputBorder(),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// IconButton(
|
||||
// onPressed: () {
|
||||
// // pointsForRiderController.onReorder(
|
||||
// // index, newIndex);
|
||||
// },
|
||||
// icon: const Icon(Icons.reorder),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ElevatedButton(
|
||||
// onPressed: () {
|
||||
// pointsForRiderController.addLocation('location');
|
||||
// },
|
||||
// child: const Text('Add Text Field'),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ));
|
||||
}
|
||||
|
||||
void showAddLocationDialog(BuildContext context, int index) {
|
||||
// Get.put(WayPointController());
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Dialog.fullscreen(
|
||||
// title: const Text('Add Location'),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Add Location'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Icon(
|
||||
Icons.clear,
|
||||
color: AppColor.secondaryColor,
|
||||
)
|
||||
],
|
||||
),
|
||||
// SizedBox(
|
||||
// width: Get.width,
|
||||
// child: formSearchCaptain(),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class AppBarPointsPageForRider extends StatelessWidget {
|
||||
const AppBarPointsPageForRider({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
maxRadius: 15,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: AppColor.secondaryColor,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
"Switch Rider".tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
Icons.clear,
|
||||
color: AppColor.secondaryColor,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
import 'package:siro_rider/constant/currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
// تأكد من المسارات
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/design.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import 'package:siro_rider/controller/functions/country_logic.dart';
|
||||
import '../../../controller/functions/audio_record1.dart';
|
||||
import '../../../controller/functions/launch.dart';
|
||||
import '../../../controller/functions/toast.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../../controller/home/map/ui_interactions_controller.dart';
|
||||
import '../../../controller/home/map/ride_state.dart';
|
||||
import '../../../controller/profile/profile_controller.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../views/home/profile/complaint_page.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 Obx(() {
|
||||
final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
// شرط الإظهار
|
||||
final bool isVisible =
|
||||
controller.currentRideState.value == RideState.inProgress &&
|
||||
controller.isStartAppHasRide == false;
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOutCubic,
|
||||
// تم تقليل قيمة الإخفاء لأن الارتفاع الكلي للنافذة أصبح أصغر
|
||||
bottom: isVisible ? 0 : -300,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SiroSheetSurface(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.lg, vertical: AppSpacing.md),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 1. هيدر المعلومات (سائق + سيارة + سعر)
|
||||
_buildCompactHeader(controller),
|
||||
|
||||
AppSpacing.vGapMd,
|
||||
|
||||
// خط فاصل خفيف
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 0.5,
|
||||
color: AppColor.grayColor.withValues(alpha: 0.2)),
|
||||
|
||||
AppSpacing.vGapMd,
|
||||
|
||||
// 2. الأزرار (إجراءات)
|
||||
_buildCompactActionButtons(
|
||||
context, controller, profileController, audioController),
|
||||
|
||||
// هامش سفلي بسيط لرفع الأزرار عن حافة الشاشة
|
||||
AppSpacing.vGapXs,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// --- الهيدر (بدون تغيير، ممتاز) ---
|
||||
Widget _buildCompactHeader(RideLifecycleController controller) {
|
||||
return Row(
|
||||
children: [
|
||||
// صورة السائق
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColor.primaryColor.withValues(alpha: 0.5),
|
||||
width: 1.5),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'),
|
||||
onBackgroundImageError: (_, __) => const Icon(Icons.person),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// الاسم ومعلومات السيارة
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
controller.driverName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
color: AppColor.writeColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.star, color: Colors.amber, size: 14),
|
||||
Text(
|
||||
controller.driverRate,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${controller.model}${controller.carColor.isNotEmpty ? " • ${controller.carColor.tr}" : ""} • ',
|
||||
style: TextStyle(fontSize: 12, color: AppColor.grayColor),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.writeColor.withValues(alpha: 0.05),
|
||||
border: Border.all(
|
||||
color: AppColor.grayColor.withValues(alpha: 0.2)),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
controller.licensePlate,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// السعر
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
NumberFormat('#,###').format(num.tryParse(controller.totalPassenger.toString()) ?? 0),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 16,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
Text(CurrencyHelper.currency,
|
||||
style: TextStyle(fontSize: 9, color: AppColor.grayColor)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// --- الأزرار (بدون تغيير) ---
|
||||
Widget _buildCompactActionButtons(
|
||||
BuildContext context,
|
||||
RideLifecycleController controller,
|
||||
ProfileController profileController,
|
||||
AudioRecorderController audioController) {
|
||||
final uiController = Get.find<UiInteractionsController>();
|
||||
return SizedBox(
|
||||
height: 64,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_compactBtn(
|
||||
icon: Icons.sos_rounded,
|
||||
label: 'SOS'.tr,
|
||||
color: AppColor.redColor,
|
||||
bgColor: AppColor.redColor.withValues(alpha: 0.1),
|
||||
onTap: () async {
|
||||
if (box.read(BoxName.sosPhonePassenger) == null) {
|
||||
await profileController.updatField(
|
||||
'sosPhone', TextInputType.phone);
|
||||
box.write(BoxName.sosPhonePassenger,
|
||||
profileController.prfoileData['sosPhone']);
|
||||
} else {
|
||||
makePhoneCall(CountryLogic.getEmergencyNumber(
|
||||
box.read(BoxName.countryCode) ?? 'Syria'));
|
||||
}
|
||||
},
|
||||
),
|
||||
_compactBtn(
|
||||
icon: FontAwesome.whatsapp,
|
||||
label: 'WhatsApp'.tr,
|
||||
color: const Color(0xFF25D366),
|
||||
bgColor: const Color(0xFF25D366).withValues(alpha: 0.1),
|
||||
onTap: () async {
|
||||
final phone = box.read(BoxName.sosPhonePassenger);
|
||||
if (phone == null || phone.toString().isEmpty) {
|
||||
// لا يوجد رقم طوارئ — نعرض الديالوج لإدخاله
|
||||
await uiController.shareTripWithFamily();
|
||||
} else {
|
||||
final formattedPhone =
|
||||
CountryLogic.formatCurrentCountryPhone(phone.toString());
|
||||
uiController.sendWhatsapp(formattedPhone);
|
||||
}
|
||||
},
|
||||
),
|
||||
_compactBtn(
|
||||
icon: Icons.share,
|
||||
label: 'Share'.tr,
|
||||
color: AppColor.primaryColor,
|
||||
bgColor: AppColor.primaryColor.withValues(alpha: 0.1),
|
||||
onTap: () async => await uiController.shareTripWithFamily(),
|
||||
),
|
||||
GetBuilder<AudioRecorderController>(
|
||||
init: audioController,
|
||||
builder: (audioCtx) {
|
||||
return _compactBtn(
|
||||
icon: audioCtx.isRecording
|
||||
? Icons.stop_circle_outlined
|
||||
: Icons.mic_none_outlined,
|
||||
label: audioCtx.isRecording ? 'Stop'.tr : 'Record'.tr,
|
||||
color: audioCtx.isRecording
|
||||
? AppColor.redColor
|
||||
: AppColor.primaryColor,
|
||||
bgColor: audioCtx.isRecording
|
||||
? AppColor.redColor.withValues(alpha: 0.1)
|
||||
: AppColor.primaryColor.withValues(alpha: 0.1),
|
||||
onTap: () async {
|
||||
if (!audioCtx.isRecording) {
|
||||
await audioCtx.startRecording(rideId: controller.rideId);
|
||||
if (context.mounted) {
|
||||
Toast.show(
|
||||
context, 'Start Record'.tr, AppColor.greenColor);
|
||||
}
|
||||
} else {
|
||||
await audioCtx.stopRecording();
|
||||
if (context.mounted) {
|
||||
Toast.show(
|
||||
context, 'Record saved'.tr, AppColor.greenColor);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
_compactBtn(
|
||||
icon: Icons.info_outline_rounded,
|
||||
label: 'Report'.tr,
|
||||
color: AppColor.grayColor,
|
||||
bgColor: AppColor.writeColor.withValues(alpha: 0.1),
|
||||
onTap: () => Get.to(() => ComplaintPage()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _compactBtn({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
required Color bgColor,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppRadii.md),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 20, color: color),
|
||||
),
|
||||
AppSpacing.vGapXs,
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColor.grayColor,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
import 'package:siro_rider/constant/currency.dart';
|
||||
import 'package:siro_rider/controller/functions/launch.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../controller/functions/country_logic.dart';
|
||||
|
||||
// ... استيراد ملفاتك الأخرى ...
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../../controller/home/map/ui_interactions_controller.dart';
|
||||
import '../../../controller/home/map/ride_state.dart';
|
||||
import '../../../controller/profile/profile_controller.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/error_snakbar.dart';
|
||||
|
||||
class RideFromStartApp extends StatelessWidget {
|
||||
const RideFromStartApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profileController = Get.put(ProfileController());
|
||||
final RideLifecycleController controller =
|
||||
Get.find<RideLifecycleController>();
|
||||
final UiInteractionsController uiController =
|
||||
Get.find<UiInteractionsController>();
|
||||
|
||||
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: BoxDecoration(
|
||||
color: AppColor.secondaryColor, // خلفية متفاعلة
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(25),
|
||||
topRight: Radius.circular(25),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Get.isDarkMode
|
||||
? Colors.black.withValues(alpha: 0.4)
|
||||
: Colors.black12,
|
||||
blurRadius: 15.0,
|
||||
spreadRadius: 5.0,
|
||||
offset: const 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: AppColor.grayColor.withValues(alpha: 0.3),
|
||||
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),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/$driverId.jpg'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// الاسم والسيارة
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
driverName,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.writeColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.star,
|
||||
color: AppColor.yellowColor, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
driverRate,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 13, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 12,
|
||||
color: AppColor.grayColor.withValues(alpha: 0.3)),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"$carType - $carModel",
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 13, color: AppColor.grayColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// حالة الرحلة (نص ملون)
|
||||
_buildStatusBadge(controller.currentRideState.value),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 3. شريط المعلومات (وقت، مسافة، سعر)
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.grayColor
|
||||
.withValues(alpha: 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 ${CurrencyHelper.currency}", "Price".tr),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 4. أزرار التحكم (SOS & Share)
|
||||
Row(
|
||||
children: [
|
||||
// زر المشاركة (يأخذ مساحة أكبر)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _checkAndCall(
|
||||
uiController.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(CountryLogic.getEmergencyNumber(box.read(BoxName.countryCode) ?? 'Syria')),
|
||||
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.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withValues(alpha: 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.primaryColor,
|
||||
size: 22), // افترضت أن السكندري لون داكن، أو استخدم Primary
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 15,
|
||||
color: AppColor.writeColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColor.grayColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVerticalDivider() {
|
||||
return Container(
|
||||
height: 30,
|
||||
width: 1,
|
||||
color: AppColor.grayColor.withValues(alpha: 0.2),
|
||||
);
|
||||
}
|
||||
|
||||
// دالة المساعدة للمنطق (بقيت كما هي ولكن تم تمرير البروفايل كونترولر)
|
||||
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 {
|
||||
mySnackbarWarning('Please set a valid SOS phone number.'.tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_rider/constant/colors.dart';
|
||||
import 'package:siro_rider/constant/design.dart';
|
||||
import 'package:siro_rider/constant/style.dart';
|
||||
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:siro_rider/controller/home/map/ride_state.dart';
|
||||
|
||||
// --- نافذة البحث عن كابتن ---
|
||||
class SearchingCaptainWindow extends StatefulWidget {
|
||||
const SearchingCaptainWindow({super.key});
|
||||
|
||||
@override
|
||||
State<SearchingCaptainWindow> createState() => _SearchingCaptainWindowState();
|
||||
}
|
||||
|
||||
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 Obx(() {
|
||||
final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
// شرط الإظهار يعتمد على حالة الرحلة مباشرة
|
||||
final bool isVisible =
|
||||
controller.currentRideState.value == RideState.searching;
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: AppDurations.base,
|
||||
curve: Curves.easeInOut,
|
||||
bottom: isVisible ? 0 : -Get.height * 0.45,
|
||||
left: 0,
|
||||
right: 0,
|
||||
// لا يوجد مقبض سحب: هذه الحالة لا تُغلق بالسحب بل بزر الإلغاء فقط
|
||||
child: SiroSheetSurface(
|
||||
showHandle: false,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// --- 1. أنيميشن الرادار ---
|
||||
_buildRadarAnimation(controller),
|
||||
AppSpacing.vGapXl,
|
||||
|
||||
// --- 2. زر الإلغاء ---
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
// استدعاء دالة الإلغاء الموحدة
|
||||
controller.changeCancelRidePageShow();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColor.writeColor,
|
||||
side: BorderSide(
|
||||
color: AppColor.writeColor.withValues(alpha: 0.3)),
|
||||
minimumSize: const Size.fromHeight(AppSizes.minTouch),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadii.md)),
|
||||
),
|
||||
child: Text('Cancel Search'.tr),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// --- ويدجت بناء أنيميشن الرادار ---
|
||||
Widget _buildRadarAnimation(RideLifecycleController 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.withValues(alpha: 0.7),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
// --- المحتوى في المنتصف ---
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
// النص يأتي مباشرة من الكنترولر
|
||||
controller.driversStatusForSearchWindow,
|
||||
style: AppStyle.headTitle.copyWith(fontSize: 20),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
AppSpacing.vGapSm,
|
||||
Text(
|
||||
'Searching for the nearest captain...'.tr,
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(color: AppColor.writeColor.withValues(alpha: 0.7)),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
AppSpacing.vGapLg,
|
||||
// مؤشر دوّار: المؤقت الرئيسي في الكنترولر هو من يقرر متى يعرض
|
||||
// حوار زيادة الأجرة، لذا هذا الجزء "غبي" بلا منطق.
|
||||
SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
color: AppColor.primaryColor,
|
||||
backgroundColor:
|
||||
AppColor.primaryColor.withValues(alpha: 0.2),
|
||||
),
|
||||
Center(
|
||||
child: Icon(
|
||||
Icons.search,
|
||||
color: AppColor.writeColor.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
import 'package:siro_rider/constant/colors.dart';
|
||||
import 'package:siro_rider/constant/style.dart';
|
||||
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:siro_rider/views/widgets/elevated_btn.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../print.dart';
|
||||
|
||||
class CupertinoDriverListWidget extends StatelessWidget {
|
||||
CupertinoDriverListWidget({super.key});
|
||||
|
||||
final RideLifecycleController mapPassengerController =
|
||||
Get.find<RideLifecycleController>();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text('Driver List'.tr), // Ensure text is properly localized
|
||||
),
|
||||
child: SafeArea(
|
||||
child: mapPassengerController.driversForMishwari.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'No drivers available at the moment. Please try again later.'
|
||||
.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 18, // Adjust the size as needed
|
||||
fontWeight: FontWeight.w600,
|
||||
color: CupertinoColors.inactiveGray, // Customize color
|
||||
),
|
||||
textAlign: TextAlign.center, // Center-align the text
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
itemCount: mapPassengerController.driversForMishwari.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
var driver =
|
||||
mapPassengerController.driversForMishwari[index];
|
||||
return Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: CupertinoListTile(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4, horizontal: 8),
|
||||
leading: ClipOval(
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: Image.network(
|
||||
'${AppLink.server}/portrate_captain_image/${driver['id']}.jpg',
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (BuildContext context,
|
||||
Widget child,
|
||||
ImageChunkEvent? loadingProgress) {
|
||||
if (loadingProgress == null) {
|
||||
return child; // Image is loaded
|
||||
}
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
value: loadingProgress
|
||||
.expectedTotalBytes !=
|
||||
null
|
||||
? loadingProgress
|
||||
.cumulativeBytesLoaded /
|
||||
(loadingProgress
|
||||
.expectedTotalBytes ??
|
||||
1)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (BuildContext context,
|
||||
Object error, StackTrace? stackTrace) {
|
||||
return const Icon(
|
||||
Icons.person,
|
||||
size: 25,
|
||||
color: AppColor.blueColor,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${driver['NAME'].toString().split(' ')[0]} ${driver['NAME'].toString().split(' ')[1]}',
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text('${'Age'.tr}: ${driver['age'].toString()}'),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(CupertinoIcons.star_fill,
|
||||
size: 16,
|
||||
color: CupertinoColors.systemYellow),
|
||||
const SizedBox(width: 4),
|
||||
Text(driver['rating']?.toStringAsFixed(1) ??
|
||||
'N/A'.tr),
|
||||
const SizedBox(width: 8),
|
||||
Text('${'Rides'.tr}: ${driver['ride_count']}'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Car'.tr}: ${driver['make']} ${driver['model']} (${driver['year']})'),
|
||||
Text('${'Plate'.tr}: ${driver['car_plate']}'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
// width: Get.width * .3,
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('${'Color'.tr}: ${driver['color']}'),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: driver['color_hex']
|
||||
.toString() ==
|
||||
'null'
|
||||
? Colors.amber
|
||||
: hexToColor(driver['color_hex']
|
||||
.toString()),
|
||||
borderRadius:
|
||||
BorderRadius.circular(4),
|
||||
border: Border.all(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Log.print(' driver["id"]: ${driver['driver_id']}');
|
||||
Get.find<RideLifecycleController>().driverIdVip =
|
||||
driver['driver_id'];
|
||||
|
||||
// Handle driver selection
|
||||
Get.defaultDialog(
|
||||
title:
|
||||
'${'Selected driver'.tr}: ${driver['NAME']}',
|
||||
content: Column(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Car'.tr}: ${driver['make']} ${driver['model']} (${driver['year']})'),
|
||||
Text(
|
||||
'${'Plate'.tr}: ${driver['car_plate']}'),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'${'Color'.tr}: ${driver['color']}'),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: driver['color_hex']
|
||||
.toString() ==
|
||||
'null'
|
||||
? Colors.amber
|
||||
: hexToColor(
|
||||
driver['color_hex']
|
||||
.toString()),
|
||||
borderRadius:
|
||||
BorderRadius.circular(4),
|
||||
border: Border.all(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'OK'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
showDateTimePickerDialog(driver);
|
||||
}));
|
||||
Log.print('${'Selected driver'.tr}: ${driver['NAME']}');
|
||||
// Get.back(); // Close the dialog
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Color hexToColor(String hexColor) {
|
||||
hexColor = hexColor.replaceAll("#", "");
|
||||
String colorString = "ff$hexColor";
|
||||
return Color(int.parse(colorString, radix: 16));
|
||||
}
|
||||
|
||||
void showDriverSelectionDialog(Map<String, dynamic> driver) {
|
||||
Get.defaultDialog(
|
||||
title: '${'Selected driver'.tr}: ${driver['name']}',
|
||||
content: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Car'.tr}: ${driver['make']} ${driver['model']} (${driver['year']})'),
|
||||
Text('${'Plate'.tr}: ${driver['car_plate']}'),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('${'Color'.tr}: ${driver['color']}'),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: driver['color_hex'].toString() == 'null'
|
||||
? Colors.amber
|
||||
: hexToColor(driver['color_hex'].toString()),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'OK'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
showDateTimePickerDialog(driver);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void showDateTimePickerDialog(Map<String, dynamic> driver) {
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: "Select date and time of trip".tr,
|
||||
content: SizedBox(
|
||||
// height: 400, // Adjust height as needed
|
||||
width: double.maxFinite,
|
||||
child: Column(
|
||||
children: [
|
||||
DateTimePickerWidget(),
|
||||
],
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Confirm Trip'.tr,
|
||||
onPressed: () async {
|
||||
DateTime selectedDateTime =
|
||||
mapPassengerController.selectedDateTime.value;
|
||||
// Save trip data and set up notifications
|
||||
Get.back();
|
||||
await mapPassengerController.saveTripData(driver, selectedDateTime);
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
kolor: AppColor.redColor,
|
||||
title: 'Cancel'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DateTimePickerWidget extends StatelessWidget {
|
||||
DateTimePickerWidget({super.key});
|
||||
|
||||
final RideLifecycleController controller = Get.find<RideLifecycleController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
transitionBetweenRoutes: false,
|
||||
automaticallyImplyLeading: false,
|
||||
middle: Text('Date and Time Picker'.tr),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Obx(() => Text(
|
||||
'${'Selected Date and Time'.tr}: ${controller.selectedDateTime.value}',
|
||||
style: const TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
)),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.dateAndTime,
|
||||
initialDateTime: controller.selectedDateTime.value,
|
||||
onDateTimeChanged: (newDateTime) {
|
||||
controller.updateDateTime(newDateTime);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
import 'package:siro_rider/constant/links.dart';
|
||||
import 'package:siro_rider/views/home/profile/complaint_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_rider/constant/box_name.dart';
|
||||
import 'package:siro_rider/controller/profile/profile_controller.dart';
|
||||
import 'package:siro_rider/main.dart';
|
||||
|
||||
import '../../../constant/colors.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/ride_lifecycle_controller.dart';
|
||||
import '../../../controller/home/map/ui_interactions_controller.dart';
|
||||
import '../../../controller/functions/country_logic.dart';
|
||||
|
||||
class VipRideBeginPassenger extends StatelessWidget {
|
||||
const VipRideBeginPassenger({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ProfileController profileController = Get.put(ProfileController());
|
||||
AudioRecorderController audioController =
|
||||
Get.put(AudioRecorderController());
|
||||
final uiController = Get.find<UiInteractionsController>();
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
if (controller.statusRideVip == 'Begin' ||
|
||||
!controller.statusRideFromStart) {
|
||||
return Positioned(
|
||||
left: 10,
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
child: Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
height: controller.statusRideVip == 'Begin' ? Get.height * .33 : 0,
|
||||
// width: 100,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg',
|
||||
),
|
||||
onBackgroundImageError: (_, __) {
|
||||
// Handle error here
|
||||
},
|
||||
backgroundColor: Colors.grey,
|
||||
child: const Icon(
|
||||
Icons.person, // Default icon or placeholder
|
||||
size: 30,
|
||||
color: Colors.white,
|
||||
), // Placeholder background color
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2),
|
||||
child: Text(
|
||||
controller.driverName,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text(
|
||||
controller.make,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(
|
||||
controller.model,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
if (controller.carColor.isNotEmpty) ...[
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(
|
||||
controller.carColor.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(3),
|
||||
child: Text(
|
||||
'vip',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${controller.driverRate} 📈',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// SizedBox(
|
||||
// height: 5,
|
||||
// ),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Container(
|
||||
width: Get.width * .15,
|
||||
decoration: AppStyle.boxDecoration,
|
||||
child: IconButton(
|
||||
onPressed: () => Get.to(
|
||||
() => ComplaintPage(),
|
||||
transition: Transition.downToUp,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.note_add,
|
||||
color: AppColor.redColor,
|
||||
),
|
||||
tooltip: ' Add Note', // Optional tooltip for clarity
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: Get.width * .15,
|
||||
decoration: AppStyle.boxDecoration,
|
||||
child: audioController.isRecording == false
|
||||
? IconButton(
|
||||
onPressed: () async {
|
||||
await audioController.startRecording(rideId: controller.rideId);
|
||||
if (context.mounted) {
|
||||
Toast.show(context, 'Start Record'.tr,
|
||||
AppColor.greenColor);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.play_circle_fill_outlined,
|
||||
color: AppColor.greenColor,
|
||||
),
|
||||
tooltip:
|
||||
' Add Note', // Optional tooltip for clarity
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: () async {
|
||||
await audioController.stopRecording();
|
||||
if (context.mounted) {
|
||||
Toast.show(context, 'Record saved'.tr,
|
||||
AppColor.greenColor);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.stop_circle,
|
||||
color: AppColor.greenColor,
|
||||
),
|
||||
tooltip:
|
||||
' Add Note', // Optional tooltip for clarity
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
width: Get.width * .15,
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
if (box.read(BoxName.sosPhonePassenger) == null) {
|
||||
{
|
||||
await profileController.updatField(
|
||||
'sosPhone', TextInputType.phone);
|
||||
box.write(BoxName.sosPhonePassenger,
|
||||
profileController.prfoileData['sosPhone']);
|
||||
}
|
||||
} else {
|
||||
makePhoneCall(CountryLogic.getEmergencyNumber(box.read(BoxName.countryCode) ?? 'Syria'));
|
||||
// box.read(BoxName.sosPhonePassenger));
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.sos_rounded,
|
||||
color: AppColor.redColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
width: Get.width * .15,
|
||||
child: 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 {
|
||||
var phone = box.read(BoxName.countryCode) ==
|
||||
'Egypt'
|
||||
? '+2${box.read(BoxName.sosPhonePassenger)}'
|
||||
: '+962${box.read(BoxName.sosPhonePassenger)}';
|
||||
uiController.sendWhatsapp(phone);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
FontAwesome.whatsapp,
|
||||
color: AppColor.greenColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
width: Get.width * .15,
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
await uiController.shareTripWithFamily();
|
||||
},
|
||||
icon: const Icon(
|
||||
AntDesign.Safety,
|
||||
color: AppColor.blueColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
// StreamCounter(),
|
||||
LinearProgressIndicator(
|
||||
backgroundColor: AppColor.accentColor,
|
||||
color:
|
||||
// controller.remainingTimeTimerRideBegin < 60
|
||||
// ? AppColor.redColor
|
||||
// :
|
||||
AppColor.greenColor,
|
||||
minHeight: 25,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
value:
|
||||
24 //controller.progressTimerRideBegin.toDouble(),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
controller.stringElapsedTimeRideBeginVip,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class StreamCounter extends StatelessWidget {
|
||||
const StreamCounter({super.key});
|
||||
|
||||
@override
|
||||
// Build the UI based on the timer value
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
return StreamBuilder<int>(
|
||||
initialData: 0,
|
||||
stream: controller.timerController.stream,
|
||||
builder: (context, snapshot) {
|
||||
// Calculate the remaining time based on the current tick
|
||||
final remainingTime = controller.durationToRide - snapshot.data!;
|
||||
|
||||
// Format the remaining time as a string
|
||||
final formattedRemainingTime =
|
||||
'${(remainingTime / 60).floor()}:${(remainingTime % 60).toString().padLeft(2, '0')}';
|
||||
|
||||
// Return the UI widgets based on the remaining time
|
||||
return Column(
|
||||
children: [
|
||||
Text(formattedRemainingTime),
|
||||
// ElevatedButton(
|
||||
// onPressed: () {
|
||||
// // Handle button press here
|
||||
// },
|
||||
// ),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_rider/constant/table_names.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/functions/toast.dart';
|
||||
import '../../../controller/home/map/location_search_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
GetBuilder<LocationSearchController> formSearchPlaces(int index) {
|
||||
return GetBuilder<LocationSearchController>(
|
||||
builder: (controller) {
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: AppColor.secondaryColor),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.only(),
|
||||
gapPadding: 4,
|
||||
borderSide: BorderSide(
|
||||
color: AppColor.redColor,
|
||||
width: 2,
|
||||
)),
|
||||
suffixIcon: const Icon(Icons.search),
|
||||
hintText: controller.hintTextwayPoint0.tr,
|
||||
hintStyle: AppStyle.title,
|
||||
hintMaxLines: 1,
|
||||
prefixIcon: IconButton(
|
||||
onPressed: () {
|
||||
controller.allTextEditingPlaces[index].clear();
|
||||
controller.clearPlaces(index);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
color: Colors.red[300],
|
||||
),
|
||||
),
|
||||
),
|
||||
controller: controller.allTextEditingPlaces[index],
|
||||
onChanged: (value) {
|
||||
if (controller.allTextEditingPlaces[index].text.length > 5) {
|
||||
controller.getPlacesListsWayPoint(index);
|
||||
mapEngine.changeHeightPlacesAll(index);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
controller.placeListResponseAll[index].isEmpty
|
||||
? InkWell(
|
||||
onTap: () {
|
||||
controller.startLocationFromMapAll[index] = true;
|
||||
controller.wayPointIndex = index;
|
||||
Get.back();
|
||||
mapEngine.changeWayPointStopsSheet();
|
||||
mapEngine.changePickerShown();
|
||||
},
|
||||
child: Text(
|
||||
'Choose from Map'.tr + ' $index'.tr,
|
||||
style:
|
||||
AppStyle.title.copyWith(color: AppColor.blueColor),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
Container(
|
||||
height: controller.placeListResponseAll[index].isNotEmpty
|
||||
? mapEngine.height
|
||||
: 0,
|
||||
color: AppColor.secondaryColor,
|
||||
child: ListView.builder(
|
||||
itemCount: controller.placeListResponseAll[index].length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
var res = controller.placeListResponseAll[index][i];
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
final double lat = res['geometry']['location']['lat'];
|
||||
final double lng = res['geometry']['location']['lng'];
|
||||
final String placeName = res['name'].toString();
|
||||
final selectedLatLng = LatLng(lat, lng);
|
||||
|
||||
mapEngine.changeHeightPlaces();
|
||||
|
||||
if (controller.currentLocationToFormPlacesAll[index] ==
|
||||
true) {
|
||||
controller.newStartPointLocation =
|
||||
rideLifecycle.passengerLocation;
|
||||
} else {
|
||||
rideLifecycle.passengerLocation =
|
||||
controller.newStartPointLocation;
|
||||
}
|
||||
|
||||
controller.menuWaypoints[index] = selectedLatLng;
|
||||
controller.menuWaypointNames[index] = placeName;
|
||||
|
||||
controller.convertHintTextPlaces(index, res);
|
||||
|
||||
final String start =
|
||||
'${rideLifecycle.passengerLocation.latitude},${rideLifecycle.passengerLocation.longitude}';
|
||||
final String dest =
|
||||
'${rideLifecycle.myDestination.latitude},${rideLifecycle.myDestination.longitude}';
|
||||
|
||||
await rideLifecycle.getDirectionMap(start, dest);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Image.network(
|
||||
res['icon'],
|
||||
width: 20,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await sql.insertMapLocation({
|
||||
'latitude': res['geometry']
|
||||
['location']['lat'],
|
||||
'longitude': res['geometry']
|
||||
['location']['lng'],
|
||||
'name': res['name'].toString(),
|
||||
'rate': res['rating'].toString(),
|
||||
}, TableName.placesFavorite);
|
||||
Toast.show(
|
||||
context,
|
||||
'${res['name']} ${'Saved Sucssefully'.tr}',
|
||||
AppColor.primaryColor);
|
||||
},
|
||||
icon: const Icon(Icons.favorite_border),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
res['name'].toString(),
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
res['vicinity'].toString(),
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
'rate',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
res['rating'].toString(),
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
thickness: 1,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user