feat: refactor financial wallet UI components and add offline map service support
This commit is contained in:
@@ -27,6 +27,7 @@ class AK {
|
||||
X.r(X.r(X.r(Env.accountSIDTwillo, cn), cC), cs);
|
||||
static final String serverAPI = X.r(X.r(X.r(Env.serverAPI, cn), cC), cs);
|
||||
static final String mapAPIKEY = Env.mapAPIKEY;
|
||||
static final String mapSaasKey = Env.mapSaasKey;
|
||||
static final String twilloRecoveryCode =
|
||||
X.r(X.r(X.r(Env.twilloRecoveryCode, cn), cC), cs);
|
||||
static final String authTokenTwillo =
|
||||
|
||||
@@ -1,33 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class AppColor {
|
||||
// --- Core Brand Colors ---
|
||||
|
||||
/// **Primary Color:** The brand's signature blue.
|
||||
static const Color primaryColor = Color(0xFF1DA1F2);
|
||||
static const Color writeColor = Color(0xff222359);
|
||||
|
||||
/// **Text/Write Color:** Dynamic based on theme.
|
||||
static Color get writeColor =>
|
||||
Get.isDarkMode ? Colors.white : const Color(0xFF1A1A1A);
|
||||
|
||||
/// **Secondary Color:** Main background color, dynamic based on theme.
|
||||
static Color get secondaryColor =>
|
||||
Get.isDarkMode ? const Color(0xFF121212) : Colors.white;
|
||||
|
||||
/// **Surface Color:** For cards and elevated elements.
|
||||
static Color get surfaceColor =>
|
||||
Get.isDarkMode ? const Color(0xFF1E1E1E) : Colors.white;
|
||||
|
||||
/// **Card Color:** Specifically for card backgrounds.
|
||||
static Color get cardColor =>
|
||||
Get.isDarkMode ? const Color(0xFF1E1E1E) : Colors.white;
|
||||
|
||||
/// **Border Color:** Subtle borders for both modes.
|
||||
static Color get borderColor =>
|
||||
Get.isDarkMode ? Colors.white10 : Colors.black12;
|
||||
|
||||
/// **Accent Color:** Greyish accent.
|
||||
static const Color accentColor = Color.fromARGB(255, 148, 140, 141);
|
||||
|
||||
// --- Neutral & Status Colors ---
|
||||
|
||||
/// **Grey Color:** Dynamic based on theme.
|
||||
static Color get grayColor =>
|
||||
Get.isDarkMode ? Colors.grey[400]! : const Color(0xFF8E8E93);
|
||||
|
||||
/// **Red Color (Error):** Clear red for alerts.
|
||||
static const Color redColor = Color(0xFFD32F2F);
|
||||
|
||||
/// **Green Color (Success):** Positive green.
|
||||
static const Color greenColor = Color(0xFF388E3C);
|
||||
|
||||
/// **Blue Color (Info):** Info text or success green variant.
|
||||
static const Color blueColor = Color(0xFF1DA1F2);
|
||||
|
||||
/// **Yellow Color (Warning):** Warm yellow.
|
||||
static const Color yellowColor = Color(0xFFFFA000);
|
||||
|
||||
// --- Tier & Social Colors ---
|
||||
|
||||
static const Color gold = Color(0xFFFFD700);
|
||||
static const Color bronze = Color(0xFFCD7F32);
|
||||
static const Color goldenBronze = Color(0xFFB87333); // Golden bronze color
|
||||
static const Color gold = Color(0xFFD4AF37);
|
||||
static const Color secondaryColor = Colors.white;
|
||||
static const Color accentColor = Colors.grey;
|
||||
static const Color greyColor = Colors.grey;
|
||||
static const Color twitterColor = Color(0xFF1DA1F2); // Twitter blue
|
||||
static const Color goldenBronze = Color(0xFFB87333);
|
||||
static const Color twitterColor = Color(0xFF1DA1F2);
|
||||
|
||||
static const Color redColor = Color(0xFFEA4335); // Google Red
|
||||
static const Color greenColor = Color(0xFF34A853); // Google Green
|
||||
static const Color blueColor = Color(0xFF1DA1F2); // Google Blue
|
||||
static const Color yellowColor = Color(0xFFFBBC05); // Google Yellow
|
||||
static Color deepPurpleAccent =
|
||||
const Color.fromARGB(255, 123, 76, 254).withOpacity(0.3);
|
||||
// --- Utility Colors ---
|
||||
|
||||
// For dynamic elements like gradients
|
||||
static List<Color> gradientStartEnd = [
|
||||
Color.fromARGB(255, 40, 158, 232), // Start with primary color
|
||||
Color.fromARGB(
|
||||
255, 44, 63, 75), // End with a slightly darker shade of Twitter blue
|
||||
];
|
||||
static Color get greyColor => grayColor;
|
||||
|
||||
static List<Color> secondaryGradientStartEnd = [
|
||||
const Color(0xFF1DA1F2), // Start with Twitter blue
|
||||
const Color(0xFF0C7ABF), // End with a slightly darker shade of Twitter blue
|
||||
];
|
||||
static Color get cyanBlue => const Color(0xFF1DA1F2);
|
||||
static Color get cyanAccent => const Color(0xFF1DA1F2).withOpacity(0.12);
|
||||
static Color get deepPurpleAccent => const Color(0xFFCE1126).withOpacity(0.1);
|
||||
|
||||
// --- Theme Helpers ---
|
||||
static Brightness get brightness => Get.isDarkMode ? Brightness.dark : Brightness.light;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// في ملف: constant/country_polygons.dart
|
||||
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
class CountryPolygons {
|
||||
// ==========================================================
|
||||
|
||||
53
lib/constant/finance_design_system.dart
Normal file
53
lib/constant/finance_design_system.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FinanceDesignSystem {
|
||||
// --- Colors ---
|
||||
static const Color primaryDark = Color(0xFF0A0E21);
|
||||
static const Color accentBlue = Color(0xFF3D5AFE);
|
||||
static const Color successGreen = Color(0xFF00C853);
|
||||
static const Color dangerRed = Color(0xFFD50000);
|
||||
static const Color backgroundColor = Color(0xFFF6F8FA);
|
||||
static const Color cardColor = Color(0xFFFFFFFF);
|
||||
static const Color textSecondary = Color(0xFF757575);
|
||||
static const Color textMuted = Color(0xFFBDBDBD);
|
||||
|
||||
// --- Gradients ---
|
||||
static const LinearGradient balanceGradient = LinearGradient(
|
||||
colors: [Color(0xFF0A0E21), Color(0xFF1A237E)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
);
|
||||
|
||||
static const LinearGradient dangerGradient = LinearGradient(
|
||||
colors: [Color(0xFFD50000), Color(0xFFFF5252)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
);
|
||||
|
||||
// --- Radius ---
|
||||
static const double mainRadius = 16.0;
|
||||
static const double cardRadius = 20.0;
|
||||
static const double buttonRadius = 12.0;
|
||||
|
||||
// --- Spacing ---
|
||||
static const double horizontalPadding = 16.0;
|
||||
static const double verticalSectionPadding = 24.0;
|
||||
|
||||
// --- Text Styles ---
|
||||
static const TextStyle balanceStyle = TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
);
|
||||
|
||||
static const TextStyle headingStyle = TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryDark,
|
||||
);
|
||||
|
||||
static const TextStyle subHeadingStyle = TextStyle(
|
||||
fontSize: 14,
|
||||
color: textSecondary,
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,11 @@ class AppLink {
|
||||
|
||||
static String locationServer =
|
||||
'https://location.intaleq.xyz/intaleq/ride/location';
|
||||
static String locationServerSide =
|
||||
'https://location.intaleq.xyz/intaleq/ride/location';
|
||||
static String mapSaasRoute = 'https://map-saas.intaleqapp.com/api/maps/route';
|
||||
static String mapSaasPlaces =
|
||||
'https://map-saas.intaleqapp.com/api/geocoding/places';
|
||||
static const String routeApiBaseUrl =
|
||||
"https://routesjo.intaleq.xyz/route/v1/driving";
|
||||
static final String endPoint = 'https://api.intaleq.xyz/intaleq_v1';
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import 'colors.dart';
|
||||
|
||||
class AppStyle {
|
||||
static TextStyle headTitle = TextStyle(
|
||||
static TextStyle get headTitle => TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 36,
|
||||
color: AppColor.accentColor,
|
||||
@@ -13,57 +13,61 @@ class AppStyle {
|
||||
// ?GoogleFonts.markaziText().fontFamily
|
||||
? GoogleFonts.markaziText().fontFamily
|
||||
: GoogleFonts.inter().fontFamily);
|
||||
static TextStyle headTitle2 = TextStyle(
|
||||
static TextStyle get headTitle2 => TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24,
|
||||
color: AppColor.writeColor,
|
||||
fontFamily: box.read(BoxName.lang) == 'ar'
|
||||
? GoogleFonts.markaziText().fontFamily
|
||||
: GoogleFonts.inter().fontFamily);
|
||||
static TextStyle title = TextStyle(
|
||||
static TextStyle get title => TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 16,
|
||||
color: AppColor.writeColor,
|
||||
fontFamily: box.read(BoxName.lang) == 'ar'
|
||||
? GoogleFonts.markaziText().fontFamily
|
||||
: GoogleFonts.inter().fontFamily);
|
||||
static TextStyle subtitle = TextStyle(
|
||||
static TextStyle get subtitle => TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
color: AppColor.writeColor,
|
||||
fontFamily: box.read(BoxName.lang) == 'ar'
|
||||
? GoogleFonts.markaziText().fontFamily
|
||||
: GoogleFonts.inter().fontFamily);
|
||||
static TextStyle number = const TextStyle(
|
||||
static TextStyle get number => TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: AppColor.writeColor,
|
||||
fontFamily: 'digit');
|
||||
|
||||
static BoxDecoration boxDecoration = const BoxDecoration(
|
||||
static BoxDecoration get boxDecoration => BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.accentColor, blurRadius: 5, offset: Offset(2, 4)),
|
||||
color: AppColor.accentColor.withValues(alpha: 0.3),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(2, 4)),
|
||||
BoxShadow(
|
||||
color: AppColor.accentColor, blurRadius: 5, offset: Offset(-2, -2))
|
||||
color: AppColor.accentColor.withValues(alpha: 0.1),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(-2, -2))
|
||||
],
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: BorderRadius.all(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.elliptical(15, 30),
|
||||
));
|
||||
static BoxDecoration boxDecoration1 = const BoxDecoration(
|
||||
static BoxDecoration get boxDecoration1 => BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color.fromARGB(255, 237, 230, 230),
|
||||
blurRadius: 5,
|
||||
offset: Offset(2, 4)),
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4)),
|
||||
BoxShadow(
|
||||
color: Color.fromARGB(255, 242, 237, 237),
|
||||
color: AppColor.primaryColor.withValues(alpha: 0.02),
|
||||
blurRadius: 5,
|
||||
offset: Offset(-2, -2))
|
||||
offset: const Offset(-2, -2))
|
||||
],
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: BorderRadius.all(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.elliptical(15, 30),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ import '../../../constant/links.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../views/home/Captin/history/history_details_page.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/encrypt_decrypt.dart';
|
||||
|
||||
class HistoryCaptainController extends GetxController {
|
||||
bool isloading = false;
|
||||
|
||||
@@ -379,16 +379,20 @@ class LoginDriverController extends GetxController {
|
||||
box.read(BoxName.tokenDriver).toString() ||
|
||||
serverData['data'][0]['fingerPrint'].toString() !=
|
||||
fingerPrint.toString()) {
|
||||
await Get.defaultDialog(
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'Device Change Detected'.tr,
|
||||
middleText: 'Please verify your identity'.tr,
|
||||
textConfirm: 'Verify'.tr,
|
||||
confirmTextColor: Colors.white,
|
||||
onConfirm: () {
|
||||
Get.back();
|
||||
// انتقل لصفحة OTP الجديدة
|
||||
Get.to(
|
||||
// نغلق الـ Dialog أولاً بشكل صريح
|
||||
if (Get.isDialogOpen ?? false) {
|
||||
Get.back();
|
||||
}
|
||||
|
||||
// ثم ننتقل لصفحة OTP
|
||||
Get.offAll(
|
||||
() => OtpVerificationPage(
|
||||
phone: d['phone'].toString(),
|
||||
deviceToken: fingerPrint.toString(),
|
||||
@@ -399,6 +403,9 @@ class LoginDriverController extends GetxController {
|
||||
);
|
||||
},
|
||||
);
|
||||
isloading = false;
|
||||
update();
|
||||
return true; // نخرج من الدالة هنا لنسمح لـ OTP بالتعامل مع الأمر
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart' as IO;
|
||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay;
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:geolocator/geolocator.dart' as geo;
|
||||
import '../../constant/box_name.dart';
|
||||
import '../firebase/local_notification.dart';
|
||||
|
||||
const String notificationChannelId = 'driver_service_channel';
|
||||
const int notificationId = 888;
|
||||
@@ -35,14 +37,18 @@ Future<bool> onStart(ServiceInstance service) async {
|
||||
IO.OptionBuilder()
|
||||
.setTransports(['websocket'])
|
||||
.disableAutoConnect()
|
||||
.setQuery({'driver_id': driverId, 'token': token})
|
||||
.setQuery({
|
||||
'driver_id': driverId,
|
||||
'token': token,
|
||||
'EIO': '3', // توافقية مع Workerman
|
||||
})
|
||||
.setReconnectionAttempts(double.infinity)
|
||||
.build());
|
||||
|
||||
socket.connect();
|
||||
|
||||
socket.onConnect((_) {
|
||||
print("✅ Background Service: Socket Connected!");
|
||||
print("✅ Background Service: Socket Connected! ID: ${socket?.id}");
|
||||
if (service is AndroidServiceInstance) {
|
||||
flutterLocalNotificationsPlugin.show(
|
||||
id: notificationId,
|
||||
@@ -70,39 +76,94 @@ Future<bool> onStart(ServiceInstance service) async {
|
||||
final box = GetStorage();
|
||||
bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false;
|
||||
|
||||
// 🔥 Check إضافي: هل الـ Overlay مفتوح بالفعل؟
|
||||
bool overlayActive = await Overlay.FlutterOverlayWindow.isActive();
|
||||
// 🔥 Check إضافي: هل الـ Overlay مفتوح بالفعل؟ (للأندرويد فقط)
|
||||
bool overlayActive = false;
|
||||
if (Platform.isAndroid) {
|
||||
overlayActive = await Overlay.FlutterOverlayWindow.isActive();
|
||||
}
|
||||
|
||||
if (isAppInForeground || overlayActive) {
|
||||
print("🛑 App is FOREGROUND or Overlay already shown. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
// عرض الـ Overlay
|
||||
print("🚀 App is BACKGROUND. Showing Overlay...");
|
||||
try {
|
||||
await Overlay.FlutterOverlayWindow.showOverlay(
|
||||
enableDrag: true,
|
||||
overlayTitle: "طلب جديد",
|
||||
overlayContent: "لديك طلب جديد وصل للتو!",
|
||||
flag: OverlayFlag.focusPointer,
|
||||
positionGravity: PositionGravity.auto,
|
||||
height: WindowSize.matchParent,
|
||||
width: WindowSize.matchParent,
|
||||
startPosition: const OverlayPosition(0, -30),
|
||||
);
|
||||
await Overlay.FlutterOverlayWindow.shareData(data);
|
||||
} catch (e) {
|
||||
print("Overlay Error: $e");
|
||||
// عرض الـ Overlay (للأندرويد فقط)
|
||||
if (Platform.isAndroid) {
|
||||
print("🚀 App is BACKGROUND. Showing Overlay...");
|
||||
try {
|
||||
await Overlay.FlutterOverlayWindow.showOverlay(
|
||||
enableDrag: true,
|
||||
overlayTitle: "طلب جديد",
|
||||
overlayContent: "لديك طلب جديد وصل للتو!",
|
||||
flag: OverlayFlag.focusPointer,
|
||||
positionGravity: PositionGravity.auto,
|
||||
height: WindowSize.matchParent,
|
||||
width: WindowSize.matchParent,
|
||||
startPosition: const OverlayPosition(0, -30),
|
||||
);
|
||||
await Overlay.FlutterOverlayWindow.shareData(data);
|
||||
} catch (e) {
|
||||
print("Overlay Error: $e");
|
||||
}
|
||||
} else if (Platform.isIOS) {
|
||||
// على iOS، نظهر إشعاراً عادياً لأن الـ Overlay غير موجود
|
||||
flutterLocalNotificationsPlugin.show(
|
||||
id: 1002,
|
||||
title: "طلب رحلة جديد 🚖",
|
||||
body: "لديك طلب رحلة جديد، افتح التطبيق للموافقة عليه",
|
||||
notificationDetails: const NotificationDetails(
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
),
|
||||
),
|
||||
payload: jsonEncode(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
service.on('stopService').listen((event) {
|
||||
socket?.disconnect();
|
||||
socket?.clearListeners();
|
||||
socket?.dispose();
|
||||
service.stopSelf();
|
||||
});
|
||||
|
||||
// 🔥 Location management in background isolate (Using Geolocator)
|
||||
geo.Position? latestPos;
|
||||
|
||||
// Listen to location changes continuously in the background
|
||||
geo.Geolocator.getPositionStream(
|
||||
locationSettings: geo.AndroidSettings(
|
||||
accuracy: geo.LocationAccuracy.high,
|
||||
distanceFilter: 10,
|
||||
intervalDuration: const Duration(seconds: 10),
|
||||
),
|
||||
).listen((pos) {
|
||||
latestPos = pos;
|
||||
});
|
||||
|
||||
// 🔥 MERCY HEARTBEAT: Send location every 2 minutes to keep driver active in 'raids'
|
||||
Timer.periodic(const Duration(minutes: 2), (timer) async {
|
||||
if (socket != null && socket.connected && latestPos != null) {
|
||||
try {
|
||||
socket.emit('update_location', {
|
||||
'driver_id': driverId,
|
||||
'lat': latestPos!.latitude,
|
||||
'lng': latestPos!.longitude,
|
||||
'heading': latestPos!.heading,
|
||||
'speed': latestPos!.speed * 3.6,
|
||||
'status': box.read(BoxName.statusDriverLocation) ?? 'on',
|
||||
'source': 'background_heartbeat'
|
||||
});
|
||||
print(
|
||||
"💓 Background Mercy Heartbeat Sent: ${latestPos!.latitude}, ${latestPos!.longitude}");
|
||||
} catch (e) {
|
||||
print("❌ Background Heartbeat Error: $e");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||
if (service is AndroidServiceInstance) {
|
||||
if (await service.isForegroundService()) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/*
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
@@ -234,3 +235,4 @@ class CameraClassController extends GetxController {
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -604,6 +604,59 @@ class CRUD {
|
||||
);
|
||||
return json.decode(response.body);
|
||||
}
|
||||
|
||||
Future<dynamic> getMapSaas({
|
||||
required String link,
|
||||
}) async {
|
||||
var url = Uri.parse(link);
|
||||
try {
|
||||
var response = await http.get(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': Env.mapSaasKey,
|
||||
},
|
||||
);
|
||||
Log.print('link -MapSaas: $link');
|
||||
Log.print('response -MapSaas: ${response.body}');
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Log.print('MapSaas Error: ${response.statusCode} - ${response.body}');
|
||||
return null;
|
||||
} catch (e) {
|
||||
Log.print('MapSaas Exception: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> postMapSaas({
|
||||
required String link,
|
||||
required Map<String, dynamic> payload,
|
||||
}) async {
|
||||
var url = Uri.parse(link);
|
||||
try {
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: jsonEncode(payload),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': Env.mapSaasKey,
|
||||
},
|
||||
);
|
||||
Log.print('post -MapSaas link: $link');
|
||||
Log.print('post -MapSaas payload: $payload');
|
||||
Log.print('post -MapSaas response: ${response.body}');
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Log.print('MapSaas Post Error: ${response.statusCode} - ${response.body}');
|
||||
return null;
|
||||
} catch (e) {
|
||||
Log.print('MapSaas Post Exception: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NoInternetException implements Exception {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:get/get_core/src/get_main.dart';
|
||||
import 'package:get/get_navigation/src/extension_navigation.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||
|
||||
import 'background_service.dart';
|
||||
|
||||
@@ -28,18 +32,19 @@ class PermissionsHelper {
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
|
||||
// Android 13+ (API 33+) يحتاج إذن POST_NOTIFICATIONS
|
||||
if (androidInfo.version.sdkInt >= 33) {
|
||||
final status = await Permission.notification.request();
|
||||
|
||||
if (status.isDenied) {
|
||||
print('⚠️ إذن الإشعارات مرفوض');
|
||||
mySnackbarWarning(
|
||||
"يرجى منح صلاحية الإشعارات لضمان وصول الطلبات إليك");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status.isPermanentlyDenied) {
|
||||
print('⚠️ إذن الإشعارات مرفوض بشكل دائم - افتح الإعدادات');
|
||||
await openAppSettings();
|
||||
mySnackbarWarning('يرجى فتح الإعدادات وتفعيل صلاحية الإشعارات');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -50,25 +55,31 @@ class PermissionsHelper {
|
||||
|
||||
/// طلب جميع الإذونات المطلوبة
|
||||
static Future<bool> requestAllPermissions() async {
|
||||
// إذن الإشعارات أولاً
|
||||
bool notificationGranted = await requestNotificationPermission();
|
||||
if (!notificationGranted) return false;
|
||||
// إذن الإشعارات (اختياري)
|
||||
await requestNotificationPermission();
|
||||
|
||||
// إذن الموقع
|
||||
final locationStatus = await Permission.location.request();
|
||||
if (!locationStatus.isGranted) {
|
||||
print('⚠️ إذن الموقع مرفوض');
|
||||
// 1. طلب إذن الموقع الأساسي فقط إذا كان مرفوضاً
|
||||
var status = await Permission.location.status;
|
||||
if (status.isDenied) {
|
||||
status = await Permission.location.request();
|
||||
}
|
||||
|
||||
if (status.isPermanentlyDenied) {
|
||||
_showSettingsDialog('الموقع');
|
||||
return false;
|
||||
}
|
||||
|
||||
// إذن الموقع في الخلفية
|
||||
if (Platform.isAndroid) {
|
||||
final bgLocationStatus = await Permission.locationAlways.request();
|
||||
if (!bgLocationStatus.isGranted) {
|
||||
print('⚠️ إذن الموقع في الخلفية مرفوض');
|
||||
}
|
||||
}
|
||||
return status.isGranted || status.isLimited;
|
||||
}
|
||||
|
||||
return true;
|
||||
static void _showSettingsDialog(String permissionName) {
|
||||
MyDialog().getDialog(
|
||||
'صلاحية $permissionName مطلوبة',
|
||||
'لقد قمت برفض صلاحية $permissionName سابقاً. يرجى تفعيلها من الإعدادات لتمكين التطبيق من العمل.',
|
||||
() async {
|
||||
await openAppSettings();
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:geolocator/geolocator.dart' as geo;
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:location/location.dart';
|
||||
import 'package:battery_plus/battery_plus.dart';
|
||||
import 'package:permission_handler/permission_handler.dart' as ph;
|
||||
import 'package:sefer_driver/views/home/Captin/orderCaptin/order_request_page.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart' as IO;
|
||||
import 'package:sefer_driver/constant/table_names.dart';
|
||||
import 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
|
||||
@@ -17,6 +15,7 @@ import '../../constant/box_name.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../firebase/local_notification.dart';
|
||||
import '../home/captin/home_captain_controller.dart';
|
||||
import '../home/captin/map_driver_controller.dart';
|
||||
import '../home/payment/captain_wallet_controller.dart';
|
||||
@@ -63,6 +62,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
bool _isPowerSavingMode = false;
|
||||
|
||||
final List<Map<String, dynamic>> _trackBuffer = [];
|
||||
final List<Map<String, dynamic>> _behaviorBuffer = [];
|
||||
|
||||
LatLng? _lastPosForDistance;
|
||||
LatLng? _lastRecordedRealLoc;
|
||||
@@ -137,26 +137,22 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
Log.print("📱 Lifecycle: App is in FOREGROUND");
|
||||
box.write(BoxName.isAppInForeground, true);
|
||||
|
||||
// إيقاف خدمة الخلفية لأننا في الواجهة الآن
|
||||
// إيقاف خدمة الخلفية
|
||||
BackgroundServiceHelper.stopService();
|
||||
|
||||
// التأكد من أن السوكيت متصل، وإذا لا، نعيد الاتصال فوراً
|
||||
if (socket == null || !socket!.connected) {
|
||||
Log.print("🔄 Socket disconnected in background. Reconnecting now...");
|
||||
|
||||
if (socket == null || (!socket!.connected && !_isInitializingSocket)) {
|
||||
Log.print("🔄 Initializing Socket on resume...");
|
||||
initSocket();
|
||||
} else {
|
||||
// إذا كان متصلاً، ننعش المستمعين فقط للتأكد
|
||||
_setupSocketListeners();
|
||||
}
|
||||
} else if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.detached) {
|
||||
Log.print("📱 Lifecycle: App is in BACKGROUND");
|
||||
box.write(BoxName.isAppInForeground, false);
|
||||
|
||||
// تشغيل خدمة الخلفية لضمان بقاء التطبيق حياً
|
||||
BackgroundServiceHelper.startService();
|
||||
|
||||
// ملاحظة: لا نقطع السوكيت هنا، نتركه يعمل قدر الإمكان
|
||||
// تشغيل خدمة الخلفية للأندرويد لضمان بقاء التطبيق حياً
|
||||
if (!Platform.isIOS) {
|
||||
BackgroundServiceHelper.startService();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,69 +175,107 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
// ====== Socket Logic (Improved) ======
|
||||
// ===================================================================
|
||||
|
||||
bool _isInitializingSocket = false;
|
||||
|
||||
void initSocket() {
|
||||
// منع الاستدعاءات المتداخلة التي تسبب قتل الاتصال قبل اكتماله
|
||||
if (_isInitializingSocket) {
|
||||
Log.print("⏳ Socket is already initializing. Skipping redundant call.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (socket != null && socket!.connected) {
|
||||
Log.print("✅ Socket is already connected. No need to re-init.");
|
||||
return;
|
||||
}
|
||||
|
||||
String driverId = box.read(BoxName.driverID).toString();
|
||||
String token = box.read(BoxName.tokenDriver).toString();
|
||||
String platform = Platform.isIOS ? 'ios' : 'android';
|
||||
|
||||
// 1. إذا كان السوكيت موجوداً، فقط تأكد من اتصاله
|
||||
_isInitializingSocket = true;
|
||||
|
||||
// تنظيف السوكيت القديم فقط إذا كان موجوداً وغير متصل
|
||||
if (socket != null) {
|
||||
if (!socket!.connected) {
|
||||
Log.print("🟡 Socket exists but disconnected. Reconnecting...");
|
||||
socket!.connect();
|
||||
}
|
||||
_setupSocketListeners(); // تحديث المستمعين
|
||||
return;
|
||||
Log.print("🧹 Cleaning up old socket instance...");
|
||||
socket!.clearListeners();
|
||||
socket!.dispose();
|
||||
socket = null;
|
||||
}
|
||||
|
||||
Log.print(
|
||||
"🟡 [LocationController] Creating NEW Socket for Driver: $driverId");
|
||||
"🟡 [LocationController] Initializing NEW Socket for Driver: $driverId");
|
||||
|
||||
// 2. إنشاء الاتصال
|
||||
socket = IO.io(
|
||||
'https://location.intaleq.xyz',
|
||||
IO.OptionBuilder()
|
||||
.setTransports(['websocket'])
|
||||
.enableAutoConnect() // تفعيل إعادة الاتصال التلقائي
|
||||
.setQuery(
|
||||
{'driver_id': driverId, 'token': token, 'platform': platform})
|
||||
.setReconnectionAttempts(double.infinity)
|
||||
.setReconnectionDelay(2000)
|
||||
.build());
|
||||
try {
|
||||
// العودة للـ Websocket حصراً لأنه الوحيد الذي ينجح في فتح القناة
|
||||
socket = IO.io(
|
||||
'https://location.intaleq.xyz',
|
||||
IO.OptionBuilder()
|
||||
.setTransports(['websocket'])
|
||||
.setQuery({'driver_id': driverId, 'token': token, 'EIO': '3'})
|
||||
.enableForceNew()
|
||||
.build());
|
||||
|
||||
socket!.connect();
|
||||
_setupSocketListeners();
|
||||
_setupSocketListeners();
|
||||
socket!.connect();
|
||||
} catch (e) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print("❌ Socket Initialization Exception: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// دالة منفصلة لضمان عدم تكرار المستمعين
|
||||
void _setupSocketListeners() {
|
||||
if (socket == null) return;
|
||||
|
||||
// تنظيف القديم أولاً
|
||||
socket!.off('connect');
|
||||
socket!.off('disconnect');
|
||||
socket!.off('new_ride_request');
|
||||
socket!.off('ride_cancelled');
|
||||
socket!.off('connect_error');
|
||||
socket!.off('error');
|
||||
|
||||
socket!.onConnect((_) {
|
||||
Log.print('✅ Socket Connected! ID: ${socket?.id}');
|
||||
isSocketConnected = true;
|
||||
_startHeartbeat();
|
||||
_isInitializingSocket = false;
|
||||
|
||||
// ننتظر قليلاً للتأكد من تعبئة الـ IDs
|
||||
Future.delayed(const Duration(milliseconds: 1000), () {
|
||||
String? sid = socket?.id;
|
||||
String? eid = socket?.io.engine?.id;
|
||||
|
||||
Log.print(
|
||||
'✅ Socket Connected! ID: ${sid ?? eid ?? 'N/A'} (SID: $sid, EID: $eid)');
|
||||
|
||||
if (sid != null || eid != null) {
|
||||
isSocketConnected = true;
|
||||
_startHeartbeat();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket!.onDisconnect((_) {
|
||||
Log.print('❌ Socket Disconnected');
|
||||
socket!.onDisconnect((data) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print('❌ Socket Disconnected: $data');
|
||||
isSocketConnected = false;
|
||||
_stopHeartbeat();
|
||||
});
|
||||
|
||||
socket!.onConnectError((err) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print('❌ Socket Connect Error: $err');
|
||||
});
|
||||
|
||||
socket!.onConnectTimeout((data) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print('❌ Socket Connect Timeout: $data');
|
||||
});
|
||||
|
||||
socket!.onError((err) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print('❌ Socket General Error: $err');
|
||||
});
|
||||
|
||||
socket!.on('reconnect_attempt', (attempt) {
|
||||
Log.print('🔄 Socket Reconnecting... Attempt: $attempt');
|
||||
});
|
||||
|
||||
// 🔥 الاستماع للطلبات الجديدة
|
||||
socket!.on('new_ride_request', (data) {
|
||||
Log.print("🔔 Socket: New Ride Request Arrived!");
|
||||
@@ -296,8 +330,17 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
if (!isAppInForeground) {
|
||||
Log.print(
|
||||
"🛑 التطبيق في الخلفية. السوكيت سيتجاهل التوجيه ويترك المهمة للـ Overlay.");
|
||||
return; // 👈 هذا السطر يمنع السوكيت من إكمال العمل وفتح الصفحة
|
||||
"📱 [LocationController] Order received in background (iOS/Android). Source: $source");
|
||||
|
||||
if (Platform.isIOS) {
|
||||
// على iOS، نقوم بإظهار إشعار محلي لأن الـ Overlay غير مدعوم
|
||||
NotificationController().showNotification(
|
||||
"طلب رحلة جديد 🚖",
|
||||
"لديك طلب رحلة جديد، افتح التطبيق للموافقة عليه",
|
||||
jsonEncode(rideData),
|
||||
'ding.wav');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -309,14 +352,23 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
// 3. تجهيز البيانات (DriverList)
|
||||
List<dynamic> driverList = [];
|
||||
if (rideData.length > 0) {
|
||||
var sortedKeys = rideData.keys.map((e) => int.tryParse(e) ?? 0).toList()
|
||||
..sort();
|
||||
if (rideData.isNotEmpty) {
|
||||
var sortedKeys = rideData.keys
|
||||
.where((e) => int.tryParse(e) != null)
|
||||
.map((e) => int.parse(e))
|
||||
.toList()..sort();
|
||||
|
||||
for (var key in sortedKeys) {
|
||||
driverList.add(rideData[key.toString()]);
|
||||
}
|
||||
}
|
||||
|
||||
// الحماية ضد البنية غير المكتملة
|
||||
if (driverList.length <= 16) {
|
||||
Log.print("❌ Socket Error: Parsed driver list is incomplete.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. إغلاق النافذة (إن وجدت بالخطأ) والتنقل
|
||||
try {
|
||||
if (await TripOverlayPlugin.isOverlayActive()) {
|
||||
@@ -459,21 +511,16 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
_uploadBatchTimer?.cancel();
|
||||
_socketHeartbeat?.cancel();
|
||||
|
||||
if (socket != null && socket!.connected) {
|
||||
String driverId = box.read(BoxName.driverID).toString();
|
||||
socket!.emit('update_location', {
|
||||
'driver_id': driverId,
|
||||
'lat': myLocation.latitude,
|
||||
'lng': myLocation.longitude,
|
||||
'heading': heading,
|
||||
'speed': speed * 3.6,
|
||||
'status': 'close', // Changed to off
|
||||
'distance': totalDistance
|
||||
});
|
||||
socket!.disconnect();
|
||||
if (socket != null) {
|
||||
socket!.clearListeners();
|
||||
socket!
|
||||
.dispose(); // استخدام dispose بدلاً من disconnect لضمان تحرير الموارد على iOS
|
||||
}
|
||||
|
||||
if (!Platform.isIOS) {
|
||||
await BackgroundServiceHelper.stopService();
|
||||
}
|
||||
|
||||
await BackgroundServiceHelper.stopService();
|
||||
socket = null;
|
||||
isSocketConnected = false;
|
||||
_isReady = false;
|
||||
@@ -526,16 +573,31 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
Future<void> _flushBufferToServer() async {
|
||||
if (_trackBuffer.isEmpty) return;
|
||||
List<Map<String, dynamic>> batch = List.from(_trackBuffer);
|
||||
_trackBuffer.clear();
|
||||
|
||||
int itemsToTake = _trackBuffer.length > 100 ? 100 : _trackBuffer.length;
|
||||
List<Map<String, dynamic>> batch = _trackBuffer.sublist(0, itemsToTake);
|
||||
|
||||
final String driverId = (box.read(BoxName.driverID) ?? '').toString();
|
||||
try {
|
||||
await CRUD().post(
|
||||
var res = await CRUD().post(
|
||||
link: '${AppLink.locationServer}/add_batch.php',
|
||||
payload: {'driver_id': driverId, 'batch_data': jsonEncode(batch)},
|
||||
);
|
||||
if (res != 'failure') {
|
||||
_trackBuffer.removeRange(0, itemsToTake);
|
||||
} else {
|
||||
_enforceBufferLimit();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('❌ Failed to upload batch: $e');
|
||||
_enforceBufferLimit();
|
||||
}
|
||||
}
|
||||
|
||||
void _enforceBufferLimit() {
|
||||
if (_trackBuffer.length > 500) {
|
||||
_trackBuffer.removeRange(0, _trackBuffer.length - 500);
|
||||
Log.print("⚠️ Buffer limit enforced. Removed oldest entries.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,32 +609,65 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
if (level >= powerSaveExitLevel) _isPowerSavingMode = false;
|
||||
if (previousMode != _isPowerSavingMode) {
|
||||
_startBatchTimers();
|
||||
startLocationUpdates();
|
||||
_updateLocationSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _updateLocationSettings() async {
|
||||
if (_locSub == null) return;
|
||||
int interval = _isPowerSavingMode ? 10000 : 5000;
|
||||
try {
|
||||
await location.changeSettings(
|
||||
accuracy: LocationAccuracy.navigation,
|
||||
interval: interval,
|
||||
distanceFilter: _isPowerSavingMode ? 20 : 10,
|
||||
);
|
||||
Log.print("🔋 Location settings updated. Power Save: $_isPowerSavingMode");
|
||||
} catch (e) {
|
||||
Log.print("❌ Failed to update location settings: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveBehaviorIfMoved(LatLng pos, DateTime now,
|
||||
{required double currentSpeed}) async {
|
||||
final dist =
|
||||
(_lastSqlLoc == null) ? 999.0 : _calculateDistance(_lastSqlLoc!, pos);
|
||||
if (dist < 15.0) return;
|
||||
|
||||
final accel = _calcAcceleration(currentSpeed, now) ?? 0.0;
|
||||
try {
|
||||
await sql.insertData({
|
||||
'driver_id': (box.read(BoxName.driverID) ?? '').toString(),
|
||||
'latitude': pos.latitude,
|
||||
'longitude': pos.longitude,
|
||||
'acceleration': accel,
|
||||
'created_at': now.toIso8601String(),
|
||||
'updated_at': now.toIso8601String(),
|
||||
}, TableName.behavior);
|
||||
_lastSqlLoc = pos;
|
||||
} catch (e) {
|
||||
Log.print('SQLite Error: $e');
|
||||
_lastSqlLoc = pos;
|
||||
|
||||
_behaviorBuffer.add({
|
||||
'driver_id': (box.read(BoxName.driverID) ?? '').toString(),
|
||||
'latitude': pos.latitude,
|
||||
'longitude': pos.longitude,
|
||||
'acceleration': accel,
|
||||
'created_at': now.toIso8601String(),
|
||||
'updated_at': now.toIso8601String(),
|
||||
});
|
||||
|
||||
if (_behaviorBuffer.length >= 10) {
|
||||
_flushBehaviorBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
void _flushBehaviorBuffer() {
|
||||
if (_behaviorBuffer.isEmpty) return;
|
||||
List<Map<String, dynamic>> batch = List.from(_behaviorBuffer);
|
||||
_behaviorBuffer.clear();
|
||||
|
||||
Future.microtask(() async {
|
||||
try {
|
||||
for (var data in batch) {
|
||||
await sql.insertData(data, TableName.behavior);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('SQLite Batch Insert Error: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// استبدال دالة Haversine اليدوية بـ Geolocator في باقي الكود أيضاً
|
||||
// لأنها تعتمد على C++ في الأندرويد و Obj-C في الآيفون (Native Speed)
|
||||
double _calculateDistance(LatLng a, LatLng b) {
|
||||
|
||||
@@ -217,8 +217,12 @@ class SecurityHelper {
|
||||
isNotTrust = await JailbreakRootDetection.instance.isNotTrust;
|
||||
isJailBroken = await JailbreakRootDetection.instance.isJailBroken;
|
||||
isRealDevice = await JailbreakRootDetection.instance.isRealDevice;
|
||||
isOnExternalStorage =
|
||||
await JailbreakRootDetection.instance.isOnExternalStorage;
|
||||
|
||||
// This method is only relevant/implemented for Android
|
||||
if (Platform.isAndroid) {
|
||||
isOnExternalStorage =
|
||||
await JailbreakRootDetection.instance.isOnExternalStorage;
|
||||
}
|
||||
|
||||
List<JailbreakIssue> issues =
|
||||
await JailbreakRootDetection.instance.checkForIssues;
|
||||
@@ -230,7 +234,6 @@ class SecurityHelper {
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
bundleId = packageInfo.packageName;
|
||||
if (bundleId.isNotEmpty) {
|
||||
// Pass the CORRECT bundle ID to isTampered
|
||||
isTampered = await JailbreakRootDetection.instance.isTampered(bundleId);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -3,15 +3,9 @@ import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:ui' as ui; // للألوان
|
||||
// import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
// import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
// import 'package:latlong2/latlong.dart'
|
||||
// as latlng; // هذا مهم جداً للتعامل مع إحداثيات OSM
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import '../../../constant/links.dart';
|
||||
@@ -35,7 +29,8 @@ class HomeCaptainController extends GetxController {
|
||||
Timer? activeTimer;
|
||||
Map data = {};
|
||||
bool isHomeMapActive = true;
|
||||
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
|
||||
InlqBitmap carIcon = InlqBitmap.defaultMarker;
|
||||
bool isMapReadyForCommands = false;
|
||||
bool isLoading = true;
|
||||
late double kazan = 0;
|
||||
double latePrice = 0;
|
||||
@@ -55,7 +50,8 @@ class HomeCaptainController extends GetxController {
|
||||
String totalMoneyInSEFER = '0';
|
||||
String totalDurationToday = '0';
|
||||
Timer? timer;
|
||||
late LatLng myLocation = const LatLng(33.5138, 36.2765);
|
||||
Timer? _cameraFollowTimer;
|
||||
LatLng myLocation = const LatLng(33.5138, 36.2765);
|
||||
String totalPoints = '0';
|
||||
String countRefuse = '0';
|
||||
bool mapType = false;
|
||||
@@ -81,10 +77,13 @@ class HomeCaptainController extends GetxController {
|
||||
// دالة لتغيير حالة الهيت ماب (عرض/إخفاء)
|
||||
void toggleHeatmap() async {
|
||||
isHeatmapVisible = !isHeatmapVisible;
|
||||
print("🔥 [Heatmap] Visibility toggled to: $isHeatmapVisible");
|
||||
if (isHeatmapVisible) {
|
||||
await fetchAndDrawHeatmap();
|
||||
startHeatmapCycle();
|
||||
} else {
|
||||
_heatmapTimer?.cancel();
|
||||
heatmapPolygons.clear();
|
||||
print("🧹 [Heatmap] Polygons cleared.");
|
||||
}
|
||||
update(); // تحديث الواجهة
|
||||
}
|
||||
@@ -96,6 +95,7 @@ class HomeCaptainController extends GetxController {
|
||||
|
||||
// دالة جلب البيانات ورسم الخريطة
|
||||
Future<void> fetchAndDrawHeatmap() async {
|
||||
print("🚀 [Heatmap] Fetching live data...");
|
||||
// استخدم الرابط المباشر لملف JSON لسرعة قصوى
|
||||
final String jsonUrl =
|
||||
"https://api.intaleq.xyz/intaleq/ride/rides/heatmap_live.json";
|
||||
@@ -107,20 +107,26 @@ class HomeCaptainController extends GetxController {
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = json.decode(response.body);
|
||||
print("✅ [Heatmap] Data received. Points count: ${data.length}");
|
||||
_generatePolygons(data);
|
||||
} else {
|
||||
print("⚠️ [Heatmap] Server error: ${response.statusCode}");
|
||||
}
|
||||
} catch (e) {
|
||||
print("Heatmap Error: $e");
|
||||
print("❌ [Heatmap] Error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void _generatePolygons(List<dynamic> data) {
|
||||
print("🎨 [Heatmap] Processing polygons...");
|
||||
Set<Polygon> tempPolygons = {};
|
||||
|
||||
// الأوفست لرسم المربع (نصف حجم الشبكة)
|
||||
// الشبكة دقتها 0.01 درجة، لذا نصفها 0.005
|
||||
double offset = 0.005;
|
||||
|
||||
int highCount = 0, medCount = 0, lowCount = 0;
|
||||
|
||||
for (var point in data) {
|
||||
double lat = double.parse(point['lat'].toString());
|
||||
double lng = double.parse(point['lng'].toString());
|
||||
@@ -133,24 +139,27 @@ class HomeCaptainController extends GetxController {
|
||||
|
||||
// 🧠 منطق الألوان: ندمج الذكاء (Intensity) مع العدد (Count)
|
||||
if (intensity == 'high' || count >= 5) {
|
||||
highCount++;
|
||||
// منطقة مشتعلة (أحمر)
|
||||
// إما فيها طلبات ضائعة (Timeout) أو فيها عدد كبير من الطلبات
|
||||
color = Colors.red.withOpacity(0.35);
|
||||
strokeColor = Colors.red.withOpacity(0.8);
|
||||
color = Colors.red.withValues(alpha: 0.35);
|
||||
strokeColor = Colors.red.withValues(alpha: 0.8);
|
||||
} else if (intensity == 'medium' || count >= 3) {
|
||||
medCount++;
|
||||
// منطقة متوسطة (برتقالي)
|
||||
color = Colors.orange.withOpacity(0.35);
|
||||
strokeColor = Colors.orange.withOpacity(0.8);
|
||||
color = Colors.orange.withValues(alpha: 0.35);
|
||||
strokeColor = Colors.orange.withValues(alpha: 0.8);
|
||||
} else {
|
||||
lowCount++;
|
||||
// منطقة خفيفة (أصفر)
|
||||
color = Colors.yellow.withOpacity(0.3);
|
||||
strokeColor = Colors.yellow.withOpacity(0.6);
|
||||
color = Colors.yellow.withValues(alpha: 0.3);
|
||||
strokeColor = Colors.yellow.withValues(alpha: 0.6);
|
||||
}
|
||||
|
||||
// رسم المربع
|
||||
tempPolygons.add(Polygon(
|
||||
polygonId: PolygonId("$lat-$lng"),
|
||||
consumeTapEvents: true, // للسماح بالضغط عليه مستقبلاً
|
||||
// consumeTapEvents: true, // للسماح بالضغط عليه مستقبلاً
|
||||
points: [
|
||||
LatLng(lat - offset, lng - offset),
|
||||
LatLng(lat + offset, lng - offset),
|
||||
@@ -164,13 +173,27 @@ class HomeCaptainController extends GetxController {
|
||||
}
|
||||
|
||||
heatmapPolygons = tempPolygons;
|
||||
print(
|
||||
"✨ [Heatmap] Rendering Done. (🔥 High: $highCount, 🟠 Med: $medCount, 🟡 Low: $lowCount)");
|
||||
print("📍 [Heatmap] Total Polygons on Map: ${heatmapPolygons.length}");
|
||||
update(); // تحديث الخريطة
|
||||
}
|
||||
|
||||
// دالة لتشغيل الخريطة الحرارية كل فترة (مثلاً عند فتح الصفحة)
|
||||
Timer? _heatmapTimer;
|
||||
|
||||
// دالة لتشغيل الخريطة الحرارية كل فترة (كل 5 دقائق) لضمان نشاط البيانات
|
||||
void startHeatmapCycle() {
|
||||
_heatmapTimer?.cancel();
|
||||
fetchAndDrawHeatmap();
|
||||
// يمكن تفعيل Timer هنا لو أردت تحديثها تلقائياً كل 5 دقائق
|
||||
|
||||
_heatmapTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
|
||||
if (isHeatmapVisible) {
|
||||
print("🔄 [Heatmap] Periodic refresh started...");
|
||||
fetchAndDrawHeatmap();
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void goToWalletFromConnect() {
|
||||
@@ -185,16 +208,8 @@ class HomeCaptainController extends GetxController {
|
||||
}
|
||||
|
||||
void addCustomCarIcon() {
|
||||
ImageConfiguration config = ImageConfiguration(
|
||||
size: const Size(30, 35), devicePixelRatio: Get.pixelRatio);
|
||||
BitmapDescriptor.asset(
|
||||
config,
|
||||
'assets/images/car.png',
|
||||
// mipmaps: false,
|
||||
).then((value) {
|
||||
carIcon = value;
|
||||
update();
|
||||
});
|
||||
carIcon = InlqBitmap.fromAsset('assets/images/car.png');
|
||||
update();
|
||||
}
|
||||
|
||||
String stringActiveDuration = '';
|
||||
@@ -259,7 +274,7 @@ class HomeCaptainController extends GetxController {
|
||||
|
||||
// 3. إظهار الديالوج المانع
|
||||
Get.defaultDialog(
|
||||
title: "حسابك مقيد مؤقتاً ⛔",
|
||||
title: "Your account is temporarily restricted ⛔".tr,
|
||||
titleStyle:
|
||||
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||
barrierDismissible: false, // 🚫 ممنوع الإغلاق بالضغط خارجاً
|
||||
@@ -269,8 +284,9 @@ class HomeCaptainController extends GetxController {
|
||||
const Icon(Icons.timer_off_outlined,
|
||||
size: 50, color: Colors.orange),
|
||||
const SizedBox(height: 15),
|
||||
const Text(
|
||||
"لقد تجاوزت حد الإلغاء المسموح به (3 مرات).\nلا يمكنك العمل حتى انتهاء العقوبة.",
|
||||
Text(
|
||||
"You have exceeded the allowed cancellation limit (3 times).\nYou cannot work until the penalty expires."
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@@ -296,11 +312,11 @@ class HomeCaptainController extends GetxController {
|
||||
? () {
|
||||
Get.back(); // إغلاق الديالوج
|
||||
box.remove(BoxName.blockUntilDate); // إزالة الحظر
|
||||
Get.snackbar("أهلاً بك", "يمكنك الآن استقبال الطلبات",
|
||||
Get.snackbar("Welcome".tr, "You can now receive orders".tr,
|
||||
backgroundColor: Colors.green);
|
||||
}
|
||||
: null, // زر معطل
|
||||
child: Text(isFinished ? "Go Online" : "انتظر انتهاء الوقت"),
|
||||
child: Text(isFinished ? "Go Online".tr : "Wait for timer".tr),
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -334,7 +350,13 @@ class HomeCaptainController extends GetxController {
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
print("🔥 [HomeCaptain] onClose called. Tearing down map resources...");
|
||||
_blockTimer?.cancel();
|
||||
activeTimer?.cancel();
|
||||
_cameraFollowTimer?.cancel();
|
||||
_heatmapTimer?.cancel();
|
||||
stopTimer();
|
||||
mapHomeCaptainController = null;
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@@ -377,7 +399,8 @@ class HomeCaptainController extends GetxController {
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok , See you Tomorrow'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
// إغلاق الديالوج والعودة قسرياً
|
||||
navigatorKey.currentState?.pop();
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
@@ -395,26 +418,40 @@ class HomeCaptainController extends GetxController {
|
||||
update();
|
||||
}
|
||||
|
||||
// late GoogleMapController mapHomeCaptainController;
|
||||
GoogleMapController? mapHomeCaptainController;
|
||||
// final locationController = Get.find<LocationController>();
|
||||
// late IntaleqMapController mapHomeCaptainController;
|
||||
IntaleqMapController? mapHomeCaptainController;
|
||||
|
||||
// --- FIX 2: Smart Map Creation ---
|
||||
void onMapCreated(GoogleMapController controller) {
|
||||
void onMapCreated(IntaleqMapController controller) {
|
||||
print("🔥 [HomeCaptain] onMapCreated started");
|
||||
mapHomeCaptainController = controller;
|
||||
|
||||
// Check actual location before moving camera
|
||||
var currentLoc = locationController.myLocation;
|
||||
if (currentLoc.latitude != 0 && currentLoc.longitude != 0) {
|
||||
controller.animateCamera(
|
||||
CameraUpdate.newLatLng(currentLoc),
|
||||
);
|
||||
} else {
|
||||
// Optional: Move to default city view instead of ocean
|
||||
controller.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(myLocation, 10),
|
||||
);
|
||||
}
|
||||
// We delay the first move to ensure the native side is fully ready
|
||||
Future.delayed(const Duration(milliseconds: 800), () {
|
||||
if (isClosed || mapHomeCaptainController == null) return;
|
||||
|
||||
try {
|
||||
var currentLoc = locationController.myLocation;
|
||||
if (currentLoc.latitude != 0 &&
|
||||
currentLoc.latitude != null &&
|
||||
!currentLoc.latitude.isNaN) {
|
||||
print(
|
||||
"🔥 [HomeCaptain] Safely moving camera to: ${currentLoc.latitude}");
|
||||
mapHomeCaptainController!.moveCamera(
|
||||
CameraUpdate.newLatLngZoom(currentLoc, 15),
|
||||
);
|
||||
} else {
|
||||
print("🔥 [HomeCaptain] Safely moving to default Damascus");
|
||||
mapHomeCaptainController!.moveCamera(
|
||||
CameraUpdate.newLatLngZoom(myLocation, 12),
|
||||
);
|
||||
}
|
||||
// Mark as ready for regular listener updates
|
||||
isMapReadyForCommands = true;
|
||||
} catch (e) {
|
||||
print("❌ [HomeCaptain] Map move failed: $e");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void savePeriod(Duration period) {
|
||||
@@ -441,7 +478,7 @@ class HomeCaptainController extends GetxController {
|
||||
await getCaptainDurationOnToday();
|
||||
String? initialDurationStr = totalDurationToday;
|
||||
|
||||
if (initialDurationStr != null) {
|
||||
if (initialDurationStr != '0') {
|
||||
// تحويل النص (01:20:30) إلى كائن Duration
|
||||
List<String> parts = initialDurationStr.split(':');
|
||||
_currentDuration = Duration(
|
||||
@@ -490,16 +527,27 @@ class HomeCaptainController extends GetxController {
|
||||
getlocation() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
// This ensures we try to get a fix, but map doesn't crash if it fails
|
||||
await locationController.getLocation();
|
||||
try {
|
||||
// ننتظر جلب الموقع مع مهلة 10 ثوانٍ لتجنب التعليق
|
||||
var locData = await locationController.getLocation().timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () => null,
|
||||
);
|
||||
|
||||
var loc = locationController.myLocation;
|
||||
if (loc.latitude != 0) {
|
||||
myLocation = loc;
|
||||
if (locData != null && locData.latitude != null) {
|
||||
myLocation = LatLng(locData.latitude!, locData.longitude!);
|
||||
print(
|
||||
"📍 [HomeCaptain] Location updated: ${myLocation.latitude}, ${myLocation.longitude}");
|
||||
} else {
|
||||
print(
|
||||
"⚠️ [HomeCaptain] Could not get current location, using default.");
|
||||
}
|
||||
} catch (e) {
|
||||
print("❌ Error in getlocation: $e");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
Map walletDriverPointsDate = {};
|
||||
@@ -535,27 +583,18 @@ class HomeCaptainController extends GetxController {
|
||||
// 4. دالة نستدعيها عند العودة للصفحة الرئيسية
|
||||
void resumeHomeMapUpdates() {
|
||||
isHomeMapActive = true;
|
||||
// إنعاش الخريطة عند العودة
|
||||
if (mapHomeCaptainController != null) {
|
||||
onMapCreated(mapHomeCaptainController!);
|
||||
}
|
||||
// تم حذف استدعاء onMapCreated المتكرر لمنع قفز الخريطة عند العودة
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
// ✅ طلب الإذونات أولاً
|
||||
bool permissionsGranted = await PermissionsHelper.requestAllPermissions();
|
||||
// ✅ تم إرجاعه كتعليق لمنع الديالوج عند التشغيل (كما كان في الكود الأصلي)
|
||||
// bool permissionsGranted = await PermissionsHelper.requestAllPermissions();
|
||||
// if (permissionsGranted) {
|
||||
// await BackgroundServiceHelper.startService();
|
||||
// }
|
||||
|
||||
if (permissionsGranted) {
|
||||
// ✅ بدء الخدمة بعد الحصول على الإذونات
|
||||
await BackgroundServiceHelper.startService();
|
||||
print('✅ Background service started successfully');
|
||||
} else {
|
||||
print('❌ لم يتم منح الإذونات - الخدمة لن تعمل');
|
||||
// اعرض رسالة للمستخدم
|
||||
}
|
||||
// await locationBackController.requestLocationPermission();
|
||||
Get.put(FirebaseMessagesController());
|
||||
addToken();
|
||||
await getlocation();
|
||||
@@ -575,29 +614,25 @@ class HomeCaptainController extends GetxController {
|
||||
checkAndShowBlockDialog();
|
||||
box.write(BoxName.statusDriverLocation, 'off');
|
||||
// 2. عدل الليسنر ليصبح مشروطاً
|
||||
locationController.addListener(() {
|
||||
// الشرط الذهبي: إذا كانت الصفحة غير نشطة أو الخريطة غير موجودة، لا تفعل شيئاً
|
||||
if (!isHomeMapActive || mapHomeCaptainController == null || isClosed)
|
||||
return;
|
||||
// 2. مؤقت التتبع التلقائي (كل 5 ثوانٍ كما في الكود السابق)
|
||||
_cameraFollowTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
|
||||
if (isClosed ||
|
||||
!isHomeMapActive ||
|
||||
mapHomeCaptainController == null ||
|
||||
!isActive) return;
|
||||
|
||||
if (isActive) {
|
||||
// isActive الخاصة بالزر "متصل/غير متصل"
|
||||
var loc = locationController.myLocation;
|
||||
if (loc.latitude != 0 && loc.longitude != 0) {
|
||||
try {
|
||||
mapHomeCaptainController!.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
target: loc,
|
||||
zoom: 17.5,
|
||||
tilt: 50.0,
|
||||
bearing: locationController.heading,
|
||||
),
|
||||
),
|
||||
var loc = locationController.myLocation;
|
||||
if (loc.latitude != 0 && loc.latitude != null && !loc.latitude.isNaN) {
|
||||
try {
|
||||
// 🔥 Safety double-check before animating
|
||||
if (mapHomeCaptainController != null) {
|
||||
print("🔥 [HomeCaptain] Safely moving camera to: ${loc.latitude}");
|
||||
mapHomeCaptainController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(loc, 17.5),
|
||||
);
|
||||
} catch (e) {
|
||||
// التقاط الخطأ بصمت إذا حدث أثناء الانتقال
|
||||
}
|
||||
} catch (e) {
|
||||
print("❌ [HomeCaptain] Camera movement failed: $e");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -724,11 +759,4 @@ class HomeCaptainController extends GetxController {
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
activeTimer?.cancel();
|
||||
stopTimer();
|
||||
mapHomeCaptainController?.dispose(); // Dispose controller
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,28 +5,21 @@ import 'dart:math';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:sefer_driver/controller/firebase/local_notification.dart';
|
||||
import 'package:sefer_driver/controller/home/captin/behavior_controller.dart';
|
||||
import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart';
|
||||
import 'package:sefer_driver/controller/home/navigation/decode_polyline_isolate.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:bubble_head/bubble.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:google_polyline_algorithm/google_polyline_algorithm.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../constant/api_key.dart';
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/country_polygons.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/table_names.dart';
|
||||
import '../../../env/env.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../../views/Rate/rate_passenger.dart';
|
||||
@@ -36,6 +29,7 @@ import '../../firebase/notification_service.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/location_controller.dart';
|
||||
import '../../functions/tts.dart';
|
||||
import 'behavior_controller.dart';
|
||||
|
||||
class MapDriverController extends GetxController {
|
||||
bool isLoading = true;
|
||||
@@ -48,10 +42,10 @@ class MapDriverController extends GetxController {
|
||||
List data = [];
|
||||
List dataDestination = [];
|
||||
LatLngBounds? boundsData;
|
||||
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
|
||||
BitmapDescriptor passengerIcon = BitmapDescriptor.defaultMarker;
|
||||
BitmapDescriptor startIcon = BitmapDescriptor.defaultMarker;
|
||||
BitmapDescriptor endIcon = BitmapDescriptor.defaultMarker;
|
||||
InlqBitmap carIcon = InlqBitmap.defaultMarker;
|
||||
InlqBitmap passengerIcon = InlqBitmap.defaultMarker;
|
||||
InlqBitmap startIcon = InlqBitmap.defaultMarker;
|
||||
InlqBitmap endIcon = InlqBitmap.defaultMarker;
|
||||
final List<LatLng> polylineCoordinates = [];
|
||||
final List<LatLng> polylineCoordinatesDestination = [];
|
||||
List<Polyline> polyLines = [];
|
||||
@@ -104,7 +98,7 @@ class MapDriverController extends GetxController {
|
||||
int remainingTimeToPassenger = 60;
|
||||
int remainingTimeInPassengerLocatioWait = 60;
|
||||
bool isDriverNearPassengerStart = false;
|
||||
GoogleMapController? mapController;
|
||||
IntaleqMapController? mapController;
|
||||
late LatLng myLocation;
|
||||
int remainingTimeTimerRideBegin = 60;
|
||||
String stringRemainingTimeRideBegin = '';
|
||||
@@ -158,16 +152,32 @@ class MapDriverController extends GetxController {
|
||||
_posSub?.cancel();
|
||||
_posSub = null;
|
||||
|
||||
mapController?.dispose();
|
||||
// mapController?.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void onMapCreated(GoogleMapController controller) {
|
||||
void onMapCreated(IntaleqMapController controller) {
|
||||
mapController = controller;
|
||||
if (Get.isRegistered<LocationController>()) {
|
||||
myLocation = Get.find<LocationController>().myLocation;
|
||||
controller.animateCamera(CameraUpdate.newLatLngZoom(myLocation, 16));
|
||||
}
|
||||
|
||||
// 🔥 رسم المسار فور جاهزية الخريطة
|
||||
if (isRideStarted) {
|
||||
// إذا كانت الرحلة بدأت، ارسم للمكان النهائي
|
||||
getRoute(
|
||||
origin: myLocation,
|
||||
destination: latLngPassengerDestination,
|
||||
routeColor: Colors.blue);
|
||||
} else {
|
||||
// إذا كان السائق ذاهب للراكب
|
||||
getRoute(
|
||||
origin: myLocation,
|
||||
destination: latLngPassengerLocation,
|
||||
routeColor: Colors.yellow);
|
||||
}
|
||||
|
||||
// بدء الاستماع للموقع للملاحة وتحديث الماركر
|
||||
startListeningStepNavigation();
|
||||
}
|
||||
@@ -239,7 +249,7 @@ class MapDriverController extends GetxController {
|
||||
}
|
||||
|
||||
takeSnapMap() {
|
||||
mapController!.takeSnapshot();
|
||||
// mapController!.takeSnapshot();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -254,7 +264,7 @@ class MapDriverController extends GetxController {
|
||||
_passengerTimer?.cancel();
|
||||
_waitingTimer?.cancel();
|
||||
_posSub?.cancel();
|
||||
mapController?.dispose();
|
||||
// mapController?.dispose();
|
||||
}
|
||||
|
||||
Future openGoogleMapFromDriverToPassenger() async {
|
||||
@@ -307,7 +317,9 @@ class MapDriverController extends GetxController {
|
||||
box.remove(BoxName.rideArguments);
|
||||
|
||||
// 3. عرض رسالة للسائق
|
||||
if (Get.isDialogOpen == true) Get.back(); // إغلاق أي ديالوج مفتوح
|
||||
if (Get.isDialogOpen == true) {
|
||||
navigatorKey.currentState?.pop();
|
||||
}
|
||||
|
||||
Get.defaultDialog(
|
||||
title: "تم إلغاء الرحلة".tr,
|
||||
@@ -325,7 +337,7 @@ class MapDriverController extends GetxController {
|
||||
),
|
||||
confirm: ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back(); // إغلاق الديالوج
|
||||
navigatorKey.currentState?.pop(); // إغلاق الديالوج
|
||||
Get.offAll(() => HomeCaptain()); // العودة للرئيسية
|
||||
},
|
||||
child: Text("OK".tr),
|
||||
@@ -367,8 +379,8 @@ class MapDriverController extends GetxController {
|
||||
box.write(BoxName.statusDriverLocation, 'blocked');
|
||||
|
||||
// عرض رسالة العقوبة
|
||||
Get.snackbar("تم تقييد حسابك مؤقتاً ⛔",
|
||||
"بسبب كثرة الإلغاءات (3 مرات)، تم إيقاف استقبال الطلبات لمدة 4 ساعات.",
|
||||
Get.snackbar("Your account is temporarily restricted ⛔".tr,
|
||||
"Due to excessive cancellations (3 times), receiving orders has been suspended for 4 hours.".tr,
|
||||
duration: Duration(seconds: 8),
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
@@ -402,15 +414,21 @@ class MapDriverController extends GetxController {
|
||||
Get.put(HomeCaptainController()).getRefusedOrderByCaptain();
|
||||
}
|
||||
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
if (Get.isDialogOpen == true) {
|
||||
navigatorKey.currentState?.pop();
|
||||
}
|
||||
Get.offAll(
|
||||
() => HomeCaptain()); // العودة للرئيسية ليتم تطبيق الحظر هناك
|
||||
} else {
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
if (Get.isDialogOpen == true) {
|
||||
navigatorKey.currentState?.pop();
|
||||
}
|
||||
Get.snackbar("Error", "Failed to cancel ride");
|
||||
}
|
||||
} catch (e) {
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
if (Get.isDialogOpen == true) {
|
||||
navigatorKey.currentState?.pop();
|
||||
}
|
||||
Log.print("Error: $e");
|
||||
}
|
||||
}
|
||||
@@ -707,7 +725,9 @@ class MapDriverController extends GetxController {
|
||||
await calculateDistanceBetweenDriverAndPassengerLocation();
|
||||
|
||||
// إغلاق مؤشر التحميل لأننا حصلنا على النتيجة
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
if (Get.isDialogOpen == true) {
|
||||
navigatorKey.currentState?.pop();
|
||||
}
|
||||
|
||||
if (distanceToPassenger < 100) {
|
||||
// زدت المسافة قليلاً لمرونة أكبر (150م)
|
||||
@@ -752,15 +772,17 @@ class MapDriverController extends GetxController {
|
||||
});
|
||||
} else {
|
||||
// --- حالة الرفض (بعيد جداً) ---
|
||||
MyDialog().getDialog(
|
||||
'You are far from passenger location'.tr,
|
||||
MyDialog().getDialog('You are far from passenger location'.tr,
|
||||
'Please go closer to the passenger location (less than 150m)'.tr,
|
||||
() => Get.back() // إغلاق الديالوج فقط
|
||||
);
|
||||
() {
|
||||
// الديالوج يغلق نفسه الآن تلقائياً
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// تنظيف اللودينج في حال حدوث خطأ غير متوقع
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
if (Get.isDialogOpen == true) {
|
||||
navigatorKey.currentState?.pop();
|
||||
}
|
||||
Log.print("Error starting ride: $e");
|
||||
Get.snackbar("Error", "Could not start ride. Please check internet.");
|
||||
}
|
||||
@@ -1448,56 +1470,23 @@ class MapDriverController extends GetxController {
|
||||
}
|
||||
|
||||
void addCustomCarIcon() {
|
||||
ImageConfiguration config = ImageConfiguration(
|
||||
size: const Size(30, 35), devicePixelRatio: Get.pixelRatio);
|
||||
BitmapDescriptor.asset(
|
||||
config,
|
||||
'assets/images/car.png',
|
||||
// mipmaps: false,
|
||||
).then((value) {
|
||||
carIcon = value;
|
||||
update();
|
||||
});
|
||||
carIcon = InlqBitmap.fromAsset('assets/images/car.png');
|
||||
update();
|
||||
}
|
||||
|
||||
void addCustomStartIcon() async {
|
||||
// Create the marker with the resized image
|
||||
|
||||
ImageConfiguration config = ImageConfiguration(
|
||||
size: const Size(30, 30), devicePixelRatio: Get.pixelRatio);
|
||||
BitmapDescriptor.asset(
|
||||
config,
|
||||
'assets/images/A.png',
|
||||
).then((value) {
|
||||
startIcon = value;
|
||||
update();
|
||||
});
|
||||
startIcon = InlqBitmap.fromAsset('assets/images/A.png');
|
||||
update();
|
||||
}
|
||||
|
||||
void addCustomEndIcon() {
|
||||
ImageConfiguration config = ImageConfiguration(
|
||||
size: const Size(25, 25), devicePixelRatio: Get.pixelRatio);
|
||||
BitmapDescriptor.asset(
|
||||
config,
|
||||
'assets/images/b.png',
|
||||
).then((value) {
|
||||
endIcon = value;
|
||||
update();
|
||||
});
|
||||
endIcon = InlqBitmap.fromAsset('assets/images/b.png');
|
||||
update();
|
||||
}
|
||||
|
||||
void addCustomPassengerIcon() {
|
||||
ImageConfiguration config = ImageConfiguration(
|
||||
size: const Size(30, 30), devicePixelRatio: Get.pixelRatio
|
||||
// scale: 1.0,
|
||||
);
|
||||
BitmapDescriptor.asset(
|
||||
config,
|
||||
'assets/images/picker.png',
|
||||
).then((value) {
|
||||
passengerIcon = value;
|
||||
update();
|
||||
});
|
||||
passengerIcon = InlqBitmap.fromAsset('assets/images/picker.png');
|
||||
update();
|
||||
}
|
||||
|
||||
var activeRouteSteps = <Map<String, dynamic>>[];
|
||||
@@ -1596,82 +1585,94 @@ class MapDriverController extends GetxController {
|
||||
required LatLng destination,
|
||||
required Color routeColor,
|
||||
}) async {
|
||||
// 1. استخدام الرابط الجديد والإعدادات الصحيحة
|
||||
String coordinates =
|
||||
'${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}';
|
||||
// استخدام الرابط من الكلاس المرجعي لأنه أحدث
|
||||
var url =
|
||||
"${AppLink.mapOSM}/route/v1/driving/$coordinates?steps=true&overview=full";
|
||||
if (mapController == null) return;
|
||||
|
||||
try {
|
||||
var response = await http.get(Uri.parse(url));
|
||||
// 1. طلب المسار من الباكيج
|
||||
final response =
|
||||
await mapController!.getDirections(origin, destination, steps: true);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var decoded = jsonDecode(response.body);
|
||||
// 2. التعامل مع الـ JSON المباشر (الذي أرسله المستخدم)
|
||||
// إذا كان الـ response يحتوي على الحقول مباشرة في الجذر
|
||||
final String? encodedPoints = response['points'];
|
||||
|
||||
if (decoded['code'] != 'Ok' || (decoded['routes'] as List).isEmpty) {
|
||||
mySnackeBarError("لم يتم العثور على مسار");
|
||||
return;
|
||||
}
|
||||
if (encodedPoints == null) {
|
||||
mySnackeBarError("No route points found".tr);
|
||||
return;
|
||||
}
|
||||
|
||||
var route = decoded['routes'][0];
|
||||
// 🔥 فك التشفير باستخدام compute لضمان أداء ممتاز
|
||||
List<LatLng> fullRoute =
|
||||
await compute(PolylineUtils.decode, encodedPoints);
|
||||
|
||||
// أ) تشغيل الـ Isolate لفك التشفير (ممتاز، ابقِ عليه)
|
||||
final String pointsString = route["geometry"];
|
||||
List<LatLng> fullRoute =
|
||||
await compute(decodePolylineIsolate, pointsString);
|
||||
if (fullRoute.isEmpty) {
|
||||
mySnackeBarError("Failed to process route points".tr);
|
||||
return;
|
||||
}
|
||||
|
||||
// ب) تهيئة المتغيرات
|
||||
upcomingPathPoints.assignAll(fullRoute);
|
||||
traveledPathPoints.clear();
|
||||
_lastTraveledIndex = 0;
|
||||
// تحديث المسافة والوقت من الـ JSON الجديد
|
||||
distance = (response['distance'] ?? 0).toString();
|
||||
duration = (response['duration'] ?? 0).toString();
|
||||
|
||||
// ج) رسم المسار الأولي
|
||||
polyLines.clear();
|
||||
polyLines.add(Polyline(
|
||||
polylineId: const PolylineId("upcoming_route"),
|
||||
points: fullRoute,
|
||||
width: 8,
|
||||
color: routeColor,
|
||||
startCap: Cap.roundCap,
|
||||
endCap: Cap.roundCap,
|
||||
));
|
||||
// ب) تهيئة المتغيرات
|
||||
upcomingPathPoints.assignAll(fullRoute);
|
||||
traveledPathPoints.clear();
|
||||
_lastTraveledIndex = 0;
|
||||
|
||||
// د) معالجة الخطوات (Legs & Steps)
|
||||
List<dynamic> legs = route['legs'];
|
||||
if (legs.isNotEmpty) {
|
||||
final stepsList = List<Map<String, dynamic>>.from(legs[0]['steps']);
|
||||
// ج) رسم المسار الأولي
|
||||
polyLines.clear();
|
||||
polyLines.add(Polyline(
|
||||
polylineId: const PolylineId("upcoming_route"),
|
||||
points: fullRoute,
|
||||
width: 8,
|
||||
color: routeColor,
|
||||
));
|
||||
|
||||
// 🔥 استخدام دالة الترجمة المحسنة من الكلاس المرجعي
|
||||
for (var step in stepsList) {
|
||||
step['html_instructions'] =
|
||||
_createInstructionFromManeuverSmart(step);
|
||||
// تصحيح مواقع المناورات
|
||||
if (step['maneuver'] != null &&
|
||||
step['maneuver']['location'] != null) {
|
||||
var loc = step['maneuver']['location'];
|
||||
// د) معالجة الخطوات (Legs & Steps) إذا توفرت في الـ JSON
|
||||
List<dynamic> legs = response['legs'] ?? [];
|
||||
if (legs.isNotEmpty) {
|
||||
final stepsList =
|
||||
List<Map<String, dynamic>>.from(legs[0]['steps'] ?? []);
|
||||
|
||||
for (var step in stepsList) {
|
||||
step['html_instructions'] = _createInstructionFromManeuverSmart(step);
|
||||
|
||||
if (step['maneuver'] != null &&
|
||||
step['maneuver']['location'] != null) {
|
||||
var loc = step['maneuver']['location'];
|
||||
// التعامل مع تنسيق OSRM [lng, lat]
|
||||
if (loc is List && loc.length >= 2) {
|
||||
step['end_location'] = {'lat': loc[1], 'lng': loc[0]};
|
||||
} else if (loc is Map) {
|
||||
step['end_location'] = loc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
routeSteps = stepsList;
|
||||
currentStepIndex = 0;
|
||||
routeSteps = stepsList;
|
||||
currentStepIndex = 0;
|
||||
|
||||
// نطق أول تعليمة
|
||||
if (routeSteps.isNotEmpty) {
|
||||
currentInstruction = routeSteps[0]['html_instructions'];
|
||||
// نطق أول تعليمة
|
||||
if (routeSteps.isNotEmpty) {
|
||||
currentInstruction = routeSteps[0]['html_instructions'];
|
||||
if (Get.isRegistered<TextToSpeechController>()) {
|
||||
Get.find<TextToSpeechController>().speakText(currentInstruction);
|
||||
}
|
||||
}
|
||||
|
||||
// هـ) تحريك الكاميرا لتشمل المسار
|
||||
if (fullRoute.isNotEmpty) {
|
||||
final bounds = _boundsFromLatLngList(fullRoute);
|
||||
safeAnimateCamera(CameraUpdate.newLatLngBounds(bounds, 80));
|
||||
}
|
||||
|
||||
update();
|
||||
} else {
|
||||
// في حال عدم وجود steps، نقوم بتصفيرها
|
||||
routeSteps = [];
|
||||
currentInstruction = "";
|
||||
}
|
||||
|
||||
// هـ) تحريك الكاميرا لتشمل المسار
|
||||
if (fullRoute.isNotEmpty) {
|
||||
final bounds = _boundsFromLatLngList(fullRoute);
|
||||
safeAnimateCamera(CameraUpdate.newLatLngBounds(bounds,
|
||||
left: 80, top: 80, right: 80, bottom: 80));
|
||||
}
|
||||
|
||||
update();
|
||||
} catch (e) {
|
||||
Log.print("Route Error: $e");
|
||||
}
|
||||
@@ -1679,7 +1680,7 @@ class MapDriverController extends GetxController {
|
||||
|
||||
// 🔥 دالة الترجمة المحسنة (من NavigationController)
|
||||
String _createInstructionFromManeuverSmart(Map<String, dynamic> step) {
|
||||
if (step['maneuver'] == null) return "تابع المسير";
|
||||
if (step['maneuver'] == null) return "Continue straight".tr;
|
||||
|
||||
final maneuver = step['maneuver'];
|
||||
final type = maneuver['type'] ?? 'continue';
|
||||
@@ -1690,10 +1691,10 @@ class MapDriverController extends GetxController {
|
||||
|
||||
switch (type) {
|
||||
case 'depart':
|
||||
instruction = "انطلق";
|
||||
instruction = "Go".tr;
|
||||
break;
|
||||
case 'arrive':
|
||||
return "لقد وصلت إلى وجهتك، $name";
|
||||
return "You have arrived at your destination, @name".trParams({'name': name});
|
||||
case 'turn':
|
||||
case 'fork':
|
||||
case 'roundabout':
|
||||
@@ -1705,14 +1706,14 @@ class MapDriverController extends GetxController {
|
||||
_getTurnInstruction(modifier); // استخدم نفس دالتك المساعدة هنا
|
||||
break;
|
||||
default:
|
||||
instruction = "تابع المسير";
|
||||
instruction = "Continue straight".tr;
|
||||
}
|
||||
|
||||
if (name.isNotEmpty) {
|
||||
if (type == 'continue') {
|
||||
instruction += " على $name";
|
||||
instruction += " ${"on".tr} $name";
|
||||
} else {
|
||||
instruction += " نحو $name";
|
||||
instruction += " ${"towards".tr} $name";
|
||||
}
|
||||
}
|
||||
return instruction;
|
||||
@@ -1728,10 +1729,10 @@ class MapDriverController extends GetxController {
|
||||
|
||||
switch (type) {
|
||||
case 'depart':
|
||||
instruction = "انطلق";
|
||||
instruction = "Go".tr;
|
||||
break;
|
||||
case 'arrive':
|
||||
instruction = "لقد وصلت إلى وجهتك";
|
||||
instruction = "You have arrived at your destination".tr;
|
||||
if (name.isNotEmpty) instruction += "، $name";
|
||||
return instruction;
|
||||
case 'turn':
|
||||
@@ -1742,20 +1743,20 @@ class MapDriverController extends GetxController {
|
||||
instruction = _getTurnInstruction(modifier);
|
||||
break;
|
||||
case 'continue':
|
||||
instruction = "استمر";
|
||||
instruction = "Continue".tr;
|
||||
break;
|
||||
default:
|
||||
instruction = "اتجه";
|
||||
instruction = "Head".tr;
|
||||
}
|
||||
|
||||
if (name.isNotEmpty) {
|
||||
if (instruction == "استمر") {
|
||||
instruction += " على $name";
|
||||
instruction += " ${"on".tr} $name";
|
||||
} else {
|
||||
instruction += " إلى $name";
|
||||
instruction += " ${"to".tr} $name";
|
||||
}
|
||||
} else if (type == 'continue' && modifier == 'straight') {
|
||||
instruction = "استمر بشكل مستقيم";
|
||||
instruction = "Continue straight".tr;
|
||||
}
|
||||
|
||||
return instruction;
|
||||
@@ -1767,23 +1768,23 @@ class MapDriverController extends GetxController {
|
||||
String _getTurnInstruction(String modifier) {
|
||||
switch (modifier) {
|
||||
case 'uturn':
|
||||
return "قم بالاستدارة والعودة";
|
||||
return "Make a U-turn".tr;
|
||||
case 'sharp right':
|
||||
return "انعطف يمينًا بحدة";
|
||||
return "Turn sharp right".tr;
|
||||
case 'right':
|
||||
return "انعطف يمينًا";
|
||||
return "Turn right".tr;
|
||||
case 'slight right':
|
||||
return "انعطف يمينًا قليلاً";
|
||||
return "Turn slight right".tr;
|
||||
case 'straight':
|
||||
return "استمر بشكل مستقيم";
|
||||
return "Continue straight".tr;
|
||||
case 'slight left':
|
||||
return "انعطف يسارًا قليلاً";
|
||||
return "Turn slight left".tr;
|
||||
case 'left':
|
||||
return "انعطف يسارًا";
|
||||
return "Turn left".tr;
|
||||
case 'sharp left':
|
||||
return "انعطف يسارًا بحدة";
|
||||
return "Turn sharp left".tr;
|
||||
default:
|
||||
return "اتجه";
|
||||
return "Head".tr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1850,7 +1851,7 @@ class MapDriverController extends GetxController {
|
||||
void _advanceStep() {
|
||||
if (currentStepIndex >= _stepBounds.length - 1) {
|
||||
// وصل للنهاية
|
||||
currentInstruction = "لقد وصلت إلى وجهتك";
|
||||
currentInstruction = "You have arrived at your destination".tr;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1909,11 +1910,11 @@ class MapDriverController extends GetxController {
|
||||
routeColor: Colors.blue);
|
||||
} else {
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
mySnackeBarError("يجب أن تكون أقرب من 100 متر للوصول");
|
||||
mySnackeBarError("You must be closer than 100 meters to arrive".tr);
|
||||
}
|
||||
} catch (e) {
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
mySnackeBarError("حدث خطأ في الاتصال");
|
||||
mySnackeBarError("A connection error occurred".tr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1941,9 +1942,7 @@ class MapDriverController extends GetxController {
|
||||
|
||||
// 2. فك تشفير البوليلاين الخاص بالخطوة وتحويله إلى LatLng
|
||||
// -->> هنا تم التصحيح <<--
|
||||
List<LatLng> pts = decodePolyline(s['polyline']['points'])
|
||||
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
|
||||
.toList();
|
||||
List<LatLng> pts = PolylineUtils.decode(s['polyline']['points']);
|
||||
|
||||
// أضف نقاط البداية والنهاية إذا لم تكن موجودة في البوليلاين لضمان دقة الحدود
|
||||
if (pts.isNotEmpty) {
|
||||
@@ -1962,14 +1961,9 @@ class MapDriverController extends GetxController {
|
||||
// A helper function to decode and convert the polyline string
|
||||
List<LatLng> decodePolylineToLatLng(String polylineString) {
|
||||
// 1. Decode the string into a list of number lists (e.g., [[lat, lng], ...])
|
||||
List<List<num>> decodedPoints = decodePolyline(polylineString);
|
||||
List<LatLng> decodedPoints = PolylineUtils.decode(polylineString);
|
||||
|
||||
// 2. Map each [lat, lng] pair to a LatLng object, ensuring conversion to double
|
||||
List<LatLng> latLngPoints = decodedPoints
|
||||
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
|
||||
.toList();
|
||||
|
||||
return latLngPoints;
|
||||
return decodedPoints;
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
@@ -1986,11 +1980,11 @@ class MapDriverController extends GetxController {
|
||||
|
||||
void _suggestOptimization() {
|
||||
Get.snackbar(
|
||||
"تحسين أداء التطبيق",
|
||||
"لضمان أفضل تجربة، نقترح تعديل الإعدادات لتناسب جهازك. هل تود المتابعة؟",
|
||||
"Improve app performance".tr,
|
||||
"To ensure the best experience, we suggest adjusting the settings to suit your device. Would you like to proceed?".tr,
|
||||
duration: const Duration(seconds: 15),
|
||||
mainButton: TextButton(
|
||||
child: const Text("نعم، قم بالتحسين"),
|
||||
child: Text("Yes, optimize".tr),
|
||||
onPressed: () {
|
||||
updateInterval.value = 8; // غير الفترة إلى 8 ثوانٍ
|
||||
// save setting to shared_preferences
|
||||
@@ -2017,7 +2011,8 @@ class MapDriverController extends GetxController {
|
||||
|
||||
Future<void> _fitToBounds(LatLngBounds b, {double padding = 60}) async {
|
||||
// نستخدم الدالة الآمنة التي أنشأناها
|
||||
await safeAnimateCamera(CameraUpdate.newLatLngBounds(b, padding));
|
||||
await safeAnimateCamera(CameraUpdate.newLatLngBounds(b,
|
||||
left: padding, top: padding, right: padding, bottom: padding));
|
||||
}
|
||||
|
||||
double distanceBetweenDriverAndPassengerWhenConfirm = 0;
|
||||
@@ -2154,7 +2149,8 @@ class MapDriverController extends GetxController {
|
||||
LatLngBounds(northeast: northeast, southwest: southwest);
|
||||
|
||||
// Fit the camera to the bounds
|
||||
var cameraUpdate = CameraUpdate.newLatLngBounds(boundsData, 140);
|
||||
var cameraUpdate = CameraUpdate.newLatLngBounds(boundsData,
|
||||
left: 140, top: 140, right: 140, bottom: 140);
|
||||
safeAnimateCamera(cameraUpdate);
|
||||
}
|
||||
|
||||
@@ -2395,22 +2391,19 @@ class MapDriverController extends GetxController {
|
||||
polyLines.removeWhere((p) => p.polylineId.value == 'upcoming_route');
|
||||
polyLines.removeWhere((p) => p.polylineId.value == 'traveled_route');
|
||||
|
||||
// المسار المتبقي (أزرق)
|
||||
// المسار المتبقي
|
||||
polyLines.add(Polyline(
|
||||
polylineId: const PolylineId('upcoming_route'),
|
||||
polylineId: const PolylineId("upcoming_route"),
|
||||
points: remaining,
|
||||
color: Colors.blue,
|
||||
width: 8,
|
||||
zIndex: 2,
|
||||
startCap: Cap.roundCap,
|
||||
endCap: Cap.roundCap,
|
||||
color: isRideStarted ? Colors.blue : Colors.yellow,
|
||||
));
|
||||
|
||||
// المسار المقطوع (رمادي)
|
||||
polyLines.add(Polyline(
|
||||
polylineId: const PolylineId('traveled_route'),
|
||||
points: traveled,
|
||||
color: Colors.grey.withOpacity(0.8),
|
||||
color: Colors.grey.withValues(alpha: 0.8),
|
||||
width: 7,
|
||||
zIndex: 1,
|
||||
));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
class NavigationStep {
|
||||
final String instruction;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:google_polyline_algorithm/google_polyline_algorithm.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:sefer_driver/constant/api_key.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/constant/links.dart';
|
||||
@@ -19,10 +18,10 @@ class NavigationService extends GetxService {
|
||||
final RxSet<Polyline> polylines = <Polyline>{}.obs;
|
||||
final RxString currentInstruction = "".obs;
|
||||
|
||||
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
|
||||
BitmapDescriptor passengerIcon = BitmapDescriptor.defaultMarker;
|
||||
BitmapDescriptor startIcon = BitmapDescriptor.defaultMarker;
|
||||
BitmapDescriptor endIcon = BitmapDescriptor.defaultMarker;
|
||||
InlqBitmap carIcon = InlqBitmap.defaultMarker;
|
||||
InlqBitmap passengerIcon = InlqBitmap.defaultMarker;
|
||||
InlqBitmap startIcon = InlqBitmap.defaultMarker;
|
||||
InlqBitmap endIcon = InlqBitmap.defaultMarker;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@@ -30,19 +29,11 @@ class NavigationService extends GetxService {
|
||||
_loadCustomIcons();
|
||||
}
|
||||
|
||||
void _loadCustomIcons() async {
|
||||
carIcon = await _createBitmapDescriptor('assets/images/car.png');
|
||||
passengerIcon = await _createBitmapDescriptor('assets/images/picker.png');
|
||||
startIcon = await _createBitmapDescriptor('assets/images/A.png');
|
||||
endIcon = await _createBitmapDescriptor('assets/images/b.png');
|
||||
}
|
||||
|
||||
Future<BitmapDescriptor> _createBitmapDescriptor(String assetName) {
|
||||
return BitmapDescriptor.fromAssetImage(
|
||||
ImageConfiguration(
|
||||
size: const Size(30, 35), devicePixelRatio: Get.pixelRatio),
|
||||
assetName,
|
||||
);
|
||||
void _loadCustomIcons() {
|
||||
carIcon = InlqBitmap.fromAsset('assets/images/car.png');
|
||||
passengerIcon = InlqBitmap.fromAsset('assets/images/picker.png');
|
||||
startIcon = InlqBitmap.fromAsset('assets/images/A.png');
|
||||
endIcon = InlqBitmap.fromAsset('assets/images/b.png');
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getRoute({
|
||||
@@ -62,9 +53,7 @@ class NavigationService extends GetxService {
|
||||
|
||||
void drawRoute(Map<String, dynamic> routeData, {Color color = Colors.blue}) {
|
||||
final pointsString = routeData["overview_polyline"]["points"];
|
||||
final points = decodePolyline(pointsString)
|
||||
.map((p) => LatLng(p[0].toDouble(), p[1].toDouble()))
|
||||
.toList();
|
||||
final points = PolylineUtils.decode(pointsString);
|
||||
|
||||
final polyline = Polyline(
|
||||
polylineId: PolylineId(routeData["summary"] ?? DateTime.now().toString()),
|
||||
|
||||
@@ -5,14 +5,16 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../env/env.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../../views/home/Captin/driver_map_page.dart';
|
||||
@@ -22,8 +24,6 @@ import '../../firebase/local_notification.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/location_controller.dart';
|
||||
import '../../home/captin/home_captain_controller.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
import '../navigation/decode_polyline_isolate.dart';
|
||||
|
||||
class OrderRequestController extends GetxController
|
||||
with WidgetsBindingObserver {
|
||||
@@ -40,7 +40,7 @@ class OrderRequestController extends GetxController
|
||||
bool _isRideTakenHandled = false;
|
||||
|
||||
// --- الأيقونات والماركرز ---
|
||||
BitmapDescriptor? driverIcon;
|
||||
InlqBitmap? driverIcon;
|
||||
Map<MarkerId, Marker> markersMap = {};
|
||||
Set<Marker> get markers => markersMap.values.toSet();
|
||||
|
||||
@@ -49,7 +49,7 @@ class OrderRequestController extends GetxController
|
||||
List<dynamic>? myList;
|
||||
Map<dynamic, dynamic>? myMapData;
|
||||
|
||||
GoogleMapController? mapController;
|
||||
IntaleqMapController? mapController;
|
||||
|
||||
// الإحداثيات (أزلنا late لتجنب الأخطاء القاتلة)
|
||||
double latPassenger = 0.0;
|
||||
@@ -64,7 +64,7 @@ class OrderRequestController extends GetxController
|
||||
String totalTripDuration = "--";
|
||||
String tripPrice = "--";
|
||||
|
||||
String timeToPassenger = "جاري الحساب...";
|
||||
String timeToPassenger = "Calculating...".tr;
|
||||
String distanceToPassenger = "--";
|
||||
|
||||
// --- الخريطة ---
|
||||
@@ -193,9 +193,25 @@ class OrderRequestController extends GetxController
|
||||
void _parseExtraData() {
|
||||
passengerRating = _safeGet(33).isEmpty ? "5.0" : _safeGet(33);
|
||||
tripType = _safeGet(31);
|
||||
totalTripDistance = _safeGet(5);
|
||||
totalTripDuration = _safeGet(19);
|
||||
tripPrice = _safeGet(2);
|
||||
|
||||
// Format numbers to avoid many decimal places
|
||||
String rawDist = _safeGet(5);
|
||||
if (rawDist.isNotEmpty) {
|
||||
double? d = double.tryParse(rawDist);
|
||||
totalTripDistance = d != null ? "${d.toStringAsFixed(1)} km" : rawDist;
|
||||
}
|
||||
|
||||
String rawDur = _safeGet(19);
|
||||
if (rawDur.isNotEmpty) {
|
||||
double? d = double.tryParse(rawDur);
|
||||
totalTripDuration = d != null ? "${d.toStringAsFixed(0)} min" : rawDur;
|
||||
}
|
||||
|
||||
String rawPrice = _safeGet(2);
|
||||
if (rawPrice.isNotEmpty) {
|
||||
double? p = double.tryParse(rawPrice);
|
||||
tripPrice = p != null ? p.toStringAsFixed(0) : rawPrice;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
@@ -203,6 +219,8 @@ class OrderRequestController extends GetxController
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
Future<void> _calculateFullJourney() async {
|
||||
if (mapController == null) return; // Wait for controller to draw
|
||||
|
||||
try {
|
||||
Position driverPos = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
@@ -210,6 +228,9 @@ class OrderRequestController extends GetxController
|
||||
|
||||
updateDriverLocation(driverLatLng, driverPos.heading);
|
||||
|
||||
// Clear old polylines to avoid "ghost lines"
|
||||
polylines.clear();
|
||||
|
||||
var pickupFuture = _fetchRouteData(
|
||||
start: driverLatLng,
|
||||
end: LatLng(latPassenger, lngPassenger),
|
||||
@@ -246,7 +267,8 @@ class OrderRequestController extends GetxController
|
||||
destTime: totalTripDuration,
|
||||
destDist: totalTripDistance);
|
||||
|
||||
zoomToFitRide(driverLatLng);
|
||||
// Now zoom to fit all polylines and markers
|
||||
zoomToFitRide();
|
||||
|
||||
update();
|
||||
} catch (e) {
|
||||
@@ -254,6 +276,22 @@ class OrderRequestController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDistance(dynamic rawDist) {
|
||||
if (rawDist == null || rawDist.toString().isEmpty) return "--";
|
||||
double dist = double.tryParse(rawDist.toString()) ?? 0.0;
|
||||
if (dist <= 0) return "--";
|
||||
if (dist < 1000) return "${dist.toStringAsFixed(0)} m";
|
||||
return "${(dist / 1000).toStringAsFixed(1)} km";
|
||||
}
|
||||
|
||||
String _formatDuration(dynamic rawDur) {
|
||||
if (rawDur == null || rawDur.toString().isEmpty) return "--";
|
||||
double dur = double.tryParse(rawDur.toString()) ?? 0.0;
|
||||
if (dur <= 0) return "1 min"; // Minimum 1 min for UI
|
||||
if (dur < 60) return "${dur.toStringAsFixed(0)} sec";
|
||||
return "${(dur / 60).toStringAsFixed(0)} min";
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> _fetchRouteData(
|
||||
{required LatLng start,
|
||||
required LatLng end,
|
||||
@@ -261,46 +299,56 @@ class OrderRequestController extends GetxController
|
||||
required String id,
|
||||
bool isDashed = false}) async {
|
||||
try {
|
||||
// حماية من الإحداثيات الصفرية
|
||||
if (start.latitude == 0 || end.latitude == 0) return null;
|
||||
if (mapController == null) return null;
|
||||
|
||||
String apiUrl = "https://routesy.intaleq.xyz/route/v1/driving";
|
||||
String coords =
|
||||
"${start.longitude},${start.latitude};${end.longitude},${end.latitude}";
|
||||
String url = "$apiUrl/$coords?steps=false&overview=full";
|
||||
final saasUrl = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: {
|
||||
'fromLat': start.latitude.toString(),
|
||||
'fromLng': start.longitude.toString(),
|
||||
'toLat': end.latitude.toString(),
|
||||
'toLng': end.longitude.toString(),
|
||||
'steps': 'false',
|
||||
'alternatives': 'false',
|
||||
});
|
||||
|
||||
var response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode == 200) {
|
||||
var json = jsonDecode(response.body);
|
||||
if (json['code'] == 'Ok' && json['routes'].isNotEmpty) {
|
||||
var route = json['routes'][0];
|
||||
final response = await http.get(saasUrl, headers: {
|
||||
'x-api-key': Env.mapSaasKey,
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
double distM = double.parse(route['distance'].toString());
|
||||
double durS = double.parse(route['duration'].toString());
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Routing request failed: ${response.statusCode}");
|
||||
}
|
||||
|
||||
String distText = "${(distM / 1000).toStringAsFixed(1)} كم";
|
||||
String durText = "${(durS / 60).toStringAsFixed(0)} دقيقة";
|
||||
final data = jsonDecode(response.body);
|
||||
print("🛣️ Route API Response [$id]: ${data}");
|
||||
|
||||
String geometry = route['geometry'];
|
||||
List<LatLng> points = await compute(decodePolylineIsolate, geometry);
|
||||
// The map-saas API returns the route data directly at the root,
|
||||
// with 'points' being an encoded polyline string.
|
||||
final String? encodedPoints = data['points']?.toString();
|
||||
|
||||
Polyline polyline = Polyline(
|
||||
polylineId: PolylineId(id),
|
||||
color: color,
|
||||
width: 5,
|
||||
points: points,
|
||||
patterns:
|
||||
isDashed ? [PatternItem.dash(10), PatternItem.gap(5)] : [],
|
||||
startCap: Cap.roundCap,
|
||||
endCap: Cap.roundCap,
|
||||
);
|
||||
if (encodedPoints != null && encodedPoints.isNotEmpty) {
|
||||
List<LatLng> path = controllerDecodePolyline(encodedPoints);
|
||||
print("📍 Path for [$id] has ${path.length} points.");
|
||||
|
||||
return {
|
||||
'distance_text': distText,
|
||||
'duration_text': durText,
|
||||
'polyline': polyline
|
||||
};
|
||||
}
|
||||
final num? rawDist = data['distance'] is num ? data['distance'] : null;
|
||||
final num? rawDur = data['duration'] is num ? data['duration'] : null;
|
||||
|
||||
final distanceText = data['distance_text'] ?? _formatDistance(rawDist);
|
||||
final durationText = data['duration_text'] ?? _formatDuration(rawDur);
|
||||
|
||||
Polyline polyline = Polyline(
|
||||
polylineId: PolylineId(id),
|
||||
color: color,
|
||||
width: 5,
|
||||
points: path,
|
||||
);
|
||||
|
||||
return {
|
||||
'distance_text': distanceText,
|
||||
'duration_text': durationText,
|
||||
'polyline': polyline
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
print("Route Fetch Error: $e");
|
||||
@@ -308,36 +356,47 @@ class OrderRequestController extends GetxController
|
||||
return null;
|
||||
}
|
||||
|
||||
void zoomToFitRide(LatLng driverPos) {
|
||||
void zoomToFitRide() {
|
||||
if (mapController == null) return;
|
||||
|
||||
// حماية من النقاط الصفرية
|
||||
if (latPassenger == 0 || latDestination == 0) return;
|
||||
List<LatLng> allPoints = [];
|
||||
|
||||
List<LatLng> points = [
|
||||
driverPos,
|
||||
LatLng(latPassenger, lngPassenger),
|
||||
LatLng(latDestination, lngDestination),
|
||||
];
|
||||
// Add all polyline points to the bounds calculation
|
||||
for (var polyline in polylines) {
|
||||
allPoints.addAll(polyline.points);
|
||||
}
|
||||
|
||||
double minLat = points.first.latitude;
|
||||
double maxLat = points.first.latitude;
|
||||
double minLng = points.first.longitude;
|
||||
double maxLng = points.first.longitude;
|
||||
// Fallback to basic markers if polylines are empty
|
||||
if (allPoints.isEmpty) {
|
||||
allPoints.addAll([
|
||||
LatLng(latPassenger, lngPassenger),
|
||||
LatLng(latDestination, lngDestination),
|
||||
]);
|
||||
}
|
||||
|
||||
for (var p in points) {
|
||||
if (allPoints.isEmpty) return;
|
||||
|
||||
double minLat = allPoints.first.latitude;
|
||||
double maxLat = allPoints.first.latitude;
|
||||
double minLng = allPoints.first.longitude;
|
||||
double maxLng = allPoints.first.longitude;
|
||||
|
||||
for (var p in allPoints) {
|
||||
if (p.latitude < minLat) minLat = p.latitude;
|
||||
if (p.latitude > maxLat) maxLat = p.latitude;
|
||||
if (p.longitude < minLng) minLng = p.longitude;
|
||||
if (p.longitude > maxLng) maxLng = p.longitude;
|
||||
}
|
||||
|
||||
// Add some padding to the bounds
|
||||
double latPad = (maxLat - minLat) * 0.25;
|
||||
double lngPad = (maxLng - minLng) * 0.2;
|
||||
|
||||
mapController!.animateCamera(CameraUpdate.newLatLngBounds(
|
||||
LatLngBounds(
|
||||
southwest: LatLng(minLat, minLng),
|
||||
northeast: LatLng(maxLat, maxLng),
|
||||
southwest: LatLng(minLat - latPad, minLng - lngPad),
|
||||
northeast: LatLng(maxLat + latPad, maxLng + lngPad),
|
||||
),
|
||||
100.0,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -357,15 +416,15 @@ class OrderRequestController extends GetxController
|
||||
// حماية إذا لم يتم جلب الإحداثيات
|
||||
if (latPassenger == 0 || latDestination == 0) return;
|
||||
|
||||
final BitmapDescriptor pickupIcon =
|
||||
final InlqBitmap pickupIcon =
|
||||
await MarkerGenerator.createCustomMarkerBitmap(
|
||||
title: paxTime,
|
||||
subtitle: paxDist,
|
||||
color: Colors.green.shade700,
|
||||
color: Colors.amber.shade900, // Matching the amber pickup line
|
||||
iconData: Icons.person_pin_circle,
|
||||
);
|
||||
|
||||
final BitmapDescriptor dropoffIcon =
|
||||
final InlqBitmap dropoffIcon =
|
||||
await MarkerGenerator.createCustomMarkerBitmap(
|
||||
title: destTime ?? totalTripDuration,
|
||||
subtitle: destDist ?? totalTripDistance,
|
||||
@@ -411,10 +470,9 @@ class OrderRequestController extends GetxController
|
||||
points: [driverLatLng, LatLng(latPassenger, lngPassenger)],
|
||||
color: Colors.grey,
|
||||
width: 2,
|
||||
patterns: [PatternItem.dash(10), PatternItem.gap(10)],
|
||||
));
|
||||
|
||||
zoomToFitRide(driverLatLng);
|
||||
zoomToFitRide();
|
||||
}
|
||||
|
||||
update();
|
||||
@@ -435,8 +493,9 @@ class OrderRequestController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
void onMapCreated(GoogleMapController controller) {
|
||||
void onMapCreated(IntaleqMapController controller) {
|
||||
mapController = controller;
|
||||
_calculateFullJourney();
|
||||
}
|
||||
|
||||
// --- قبول الطلب وإدارة التايمر ---
|
||||
@@ -488,10 +547,13 @@ class OrderRequestController extends GetxController
|
||||
// 1. حذف الإشعار من شريط التنبيهات فوراً
|
||||
NotificationController().cancelOrderNotification();
|
||||
if (Get.isSnackbarOpen) Get.closeCurrentSnackbar();
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
|
||||
// إغلاق أي ديالوج مفتوح قسرياً
|
||||
if (Get.isDialogOpen ?? false) {
|
||||
navigatorKey.currentState?.pop();
|
||||
}
|
||||
Get.back();
|
||||
Get.snackbar("تنبيه", "تم قبول الطلب من قبل سائق آخر",
|
||||
backgroundColor: Colors.orange, colorText: Colors.white);
|
||||
mySnackbarInfo("The order has been accepted by another driver.".tr);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -546,9 +608,10 @@ class OrderRequestController extends GetxController
|
||||
|
||||
if (isFailure) {
|
||||
// ⛔ حالة الفشل: الطلب مأخوذ
|
||||
MyDialog().getDialog("عذراً، الطلب أخذه سائق آخر.", '', () {
|
||||
Get.back(); // إغلاق الديالوج
|
||||
Get.back(); // العودة للصفحة الرئيسية (إغلاق صفحة الطلب)
|
||||
MyDialog().getDialog(
|
||||
"Sorry, the order was taken by another driver.".tr, '', () {
|
||||
// بما أن MyDialog يغلق نفسه الآن، نحتاج Get.back() واحدة فقط لإغلاق صفحة الطلب
|
||||
Get.back();
|
||||
});
|
||||
} else {
|
||||
// ✅ حالة النجاح
|
||||
@@ -611,7 +674,37 @@ class OrderRequestController extends GetxController
|
||||
audioPlayer.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_timer?.cancel();
|
||||
mapController?.dispose();
|
||||
// mapController?.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
List<LatLng> controllerDecodePolyline(String encoded) {
|
||||
List<LatLng> points = [];
|
||||
int index = 0, len = encoded.length;
|
||||
int lat = 0, lng = 0;
|
||||
|
||||
while (index < len) {
|
||||
int b, shift = 0, result = 0;
|
||||
do {
|
||||
b = encoded.codeUnitAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
|
||||
lat += dlat;
|
||||
|
||||
shift = 0;
|
||||
result = 0;
|
||||
do {
|
||||
b = encoded.codeUnitAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
|
||||
lng += dlng;
|
||||
|
||||
points.add(LatLng(lat / 1E5, lng / 1E5));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
// تم تعديل الدالة لتقبل وسيط من نوع `dynamic` لحل مشكلة عدم تطابق الأنواع مع دالة `compute`.
|
||||
// هذه الدالة لا تزال تعمل كدالة من المستوى الأعلى (Top-level function)
|
||||
// وهو شرط أساسي لاستخدامها مع دالة compute.
|
||||
List<LatLng> decodePolylineIsolate(dynamic encodedMessage) {
|
||||
// التأكد من أن الرسالة المستقبلة هي من نوع String
|
||||
if (encodedMessage is! String) {
|
||||
// إرجاع قائمة فارغة أو إظهار خطأ إذا كان النوع غير صحيح
|
||||
return [];
|
||||
}
|
||||
final String encoded = encodedMessage;
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
List<LatLng> decodePolylineIsolate(String encoded) {
|
||||
List<LatLng> points = [];
|
||||
int index = 0, len = encoded.length;
|
||||
int lat = 0, lng = 0;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'dart:math';
|
||||
|
||||
/// Worker entrypoint (spawnUri/spawn).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:sefer_driver/constant/style.dart';
|
||||
|
||||
import '../../constant/api_key.dart';
|
||||
|
||||
@@ -4,109 +4,57 @@ import 'package:get/get.dart';
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../main.dart';
|
||||
import '../themes/themes.dart';
|
||||
import '../profile/setting_controller.dart';
|
||||
|
||||
class LocaleController extends GetxController {
|
||||
Locale? language;
|
||||
String countryCode = '';
|
||||
|
||||
ThemeData appTheme = lightThemeEnglish;
|
||||
ThemeData get appTheme {
|
||||
String lang = box.read(BoxName.lang) ?? 'en';
|
||||
bool isDarkMode = box.read('isDarkMode') ?? false;
|
||||
return _getThemeFor(lang, isDarkMode);
|
||||
}
|
||||
|
||||
ThemeData get lightTheme {
|
||||
String lang = box.read(BoxName.lang) ?? 'en';
|
||||
return _getThemeFor(lang, false);
|
||||
}
|
||||
|
||||
ThemeData get darkTheme {
|
||||
String lang = box.read(BoxName.lang) ?? 'en';
|
||||
return _getThemeFor(lang, true);
|
||||
}
|
||||
|
||||
ThemeData _getThemeFor(String lang, bool isDarkMode) {
|
||||
if (lang.startsWith('ar')) {
|
||||
return isDarkMode ? darkThemeArabic : lightThemeArabic;
|
||||
} else {
|
||||
return isDarkMode ? darkThemeEnglish : lightThemeEnglish;
|
||||
}
|
||||
}
|
||||
|
||||
void refreshTheme() {
|
||||
update();
|
||||
}
|
||||
|
||||
void changeLang(String langcode) {
|
||||
Locale locale;
|
||||
switch (langcode) {
|
||||
case "ar":
|
||||
locale = const Locale("ar");
|
||||
appTheme = lightThemeArabic;
|
||||
box.write(BoxName.lang, 'ar');
|
||||
break;
|
||||
case "en":
|
||||
locale = const Locale("en");
|
||||
appTheme = lightThemeEnglish;
|
||||
box.write(BoxName.lang, 'en');
|
||||
break;
|
||||
case "tr":
|
||||
locale = const Locale("tr");
|
||||
appTheme = lightThemeEnglish;
|
||||
box.write(BoxName.lang, 'tr');
|
||||
break;
|
||||
case "fr":
|
||||
locale = const Locale("fr");
|
||||
appTheme = lightThemeEnglish;
|
||||
box.write(BoxName.lang, 'fr');
|
||||
break;
|
||||
case "it":
|
||||
locale = const Locale("it");
|
||||
appTheme = lightThemeEnglish;
|
||||
box.write(BoxName.lang, 'it');
|
||||
break;
|
||||
case "de":
|
||||
locale = const Locale("de");
|
||||
appTheme = lightThemeEnglish;
|
||||
box.write(BoxName.lang, 'de');
|
||||
break;
|
||||
case "el":
|
||||
locale = const Locale("el");
|
||||
appTheme = lightThemeEnglish;
|
||||
box.write(BoxName.lang, 'el');
|
||||
break;
|
||||
case "es":
|
||||
locale = const Locale("es");
|
||||
appTheme = lightThemeEnglish;
|
||||
box.write(BoxName.lang, 'es');
|
||||
break;
|
||||
case "fa":
|
||||
locale = const Locale("fa");
|
||||
appTheme = lightThemeEnglish;
|
||||
box.write(BoxName.lang, 'fa');
|
||||
break;
|
||||
case "zh":
|
||||
locale = const Locale("zh");
|
||||
appTheme = lightThemeEnglish;
|
||||
box.write(BoxName.lang, 'zh');
|
||||
break;
|
||||
case "ru":
|
||||
locale = const Locale("ru");
|
||||
appTheme = lightThemeEnglish;
|
||||
box.write(BoxName.lang, 'ru');
|
||||
break;
|
||||
case "hi":
|
||||
locale = const Locale("hi");
|
||||
appTheme = lightThemeEnglish;
|
||||
box.write(BoxName.lang, 'hi');
|
||||
break;
|
||||
case "ar-ma":
|
||||
locale = const Locale("ar-ma");
|
||||
appTheme = lightThemeArabic;
|
||||
box.write(BoxName.lang, 'ar-ma');
|
||||
break;
|
||||
case "ar-gulf":
|
||||
locale = const Locale("ar-gulf");
|
||||
appTheme = lightThemeArabic;
|
||||
box.write(BoxName.lang, 'ar-gulf');
|
||||
break;
|
||||
default:
|
||||
locale = Locale(Get.deviceLocale!.languageCode);
|
||||
box.write(BoxName.lang, Get.deviceLocale!.languageCode);
|
||||
appTheme = lightThemeEnglish;
|
||||
break;
|
||||
}
|
||||
|
||||
Locale locale = Locale(langcode);
|
||||
box.write(BoxName.lang, langcode);
|
||||
Get.changeTheme(appTheme);
|
||||
Get.updateLocale(locale);
|
||||
update();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
String? storedLang = box.read(BoxName.lang);
|
||||
if (storedLang == null) {
|
||||
// Use device language if no language is stored
|
||||
storedLang = Get.deviceLocale!.languageCode;
|
||||
box.write(BoxName.lang, storedLang);
|
||||
}
|
||||
|
||||
changeLang(storedLang);
|
||||
language = Locale(storedLang);
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,9 +18,7 @@ import '../../constant/colors.dart';
|
||||
import '../../constant/info.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../functions/crud.dart';
|
||||
import '../functions/encrypt_decrypt.dart';
|
||||
import '../functions/toast.dart';
|
||||
import 'paymob/paymob_wallet.dart';
|
||||
|
||||
@@ -170,8 +168,8 @@ class PaymentController extends GetxController {
|
||||
)),
|
||||
allowsDelayedPaymentMethods: true,
|
||||
customerEphemeralKeySecret: Stripe.merchantIdentifier,
|
||||
appearance: const PaymentSheetAppearance(
|
||||
shapes: PaymentSheetShape(borderRadius: 12),
|
||||
appearance: PaymentSheetAppearance(
|
||||
shapes: const PaymentSheetShape(borderRadius: 12),
|
||||
colors: PaymentSheetAppearanceColors(
|
||||
background: AppColor.secondaryColor,
|
||||
),
|
||||
@@ -725,7 +723,6 @@ class PaymentController extends GetxController {
|
||||
box.write(BoxName.passengerWalletTotal, '0');
|
||||
}
|
||||
// getPassengerWallet();
|
||||
final localAuth = LocalAuthentication();
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/main.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../local/local_controller.dart';
|
||||
|
||||
class SettingController extends GetxController {
|
||||
bool isGoogleMapsEnabled = false;
|
||||
bool isMapDarkMode = false;
|
||||
bool isDarkMode = false;
|
||||
|
||||
void onChangMapApp() {
|
||||
if (!isGoogleMapsEnabled) {
|
||||
isGoogleMapsEnabled = true;
|
||||
box.write(BoxName.googlaMapApp, true);
|
||||
update();
|
||||
} else {
|
||||
isGoogleMapsEnabled = false;
|
||||
box.write(BoxName.googlaMapApp, false);
|
||||
update();
|
||||
isGoogleMapsEnabled = !isGoogleMapsEnabled;
|
||||
box.write(BoxName.googlaMapApp, isGoogleMapsEnabled);
|
||||
update();
|
||||
}
|
||||
|
||||
void toggleMapTheme() {
|
||||
isMapDarkMode = !isMapDarkMode;
|
||||
box.write('isMapDarkMode', isMapDarkMode);
|
||||
update();
|
||||
}
|
||||
|
||||
void toggleAppTheme() {
|
||||
isDarkMode = !isDarkMode;
|
||||
box.write('isDarkMode', isDarkMode);
|
||||
|
||||
// Update the theme using the LocaleController to ensure correct fonts are used
|
||||
if (Get.isRegistered<LocaleController>()) {
|
||||
Get.find<LocaleController>().refreshTheme();
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
if (box.read(BoxName.googlaMapApp) != null) {
|
||||
isGoogleMapsEnabled = box.read(BoxName.googlaMapApp);
|
||||
}
|
||||
update();
|
||||
isGoogleMapsEnabled = box.read(BoxName.googlaMapApp) ?? false;
|
||||
isMapDarkMode = box.read('isMapDarkMode') ?? false;
|
||||
isDarkMode = box.read('isDarkMode') ?? false;
|
||||
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,146 +2,89 @@ import 'package:flutter/material.dart';
|
||||
import 'package:sefer_driver/constant/style.dart';
|
||||
import '../../constant/colors.dart';
|
||||
|
||||
ThemeData lightThemeEnglish = ThemeData(
|
||||
ThemeData _createTheme({
|
||||
required Brightness brightness,
|
||||
required String fontFamily,
|
||||
required Color scaffoldBackgroundColor,
|
||||
required Color cardColor,
|
||||
required Color dividerColor,
|
||||
}) {
|
||||
return ThemeData(
|
||||
brightness: brightness,
|
||||
fontFamily: fontFamily,
|
||||
scaffoldBackgroundColor: scaffoldBackgroundColor,
|
||||
cardColor: cardColor,
|
||||
dividerColor: dividerColor,
|
||||
primaryColor: AppColor.primaryColor,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: AppColor.primaryColor,
|
||||
brightness: brightness,
|
||||
primary: AppColor.primaryColor,
|
||||
surface: cardColor,
|
||||
background: scaffoldBackgroundColor,
|
||||
error: AppColor.redColor,
|
||||
),
|
||||
textTheme: TextTheme(
|
||||
displaySmall: AppStyle.title.copyWith(color: AppColor.writeColor),
|
||||
displayLarge: AppStyle.headTitle.copyWith(color: AppColor.writeColor),
|
||||
displayMedium: AppStyle.headTitle2.copyWith(color: AppColor.writeColor),
|
||||
bodyLarge: AppStyle.title.copyWith(color: AppColor.writeColor),
|
||||
bodyMedium: AppStyle.subtitle.copyWith(color: AppColor.writeColor),
|
||||
titleLarge: AppStyle.headTitle2.copyWith(color: AppColor.writeColor),
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 0,
|
||||
backgroundColor: scaffoldBackgroundColor,
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(color: AppColor.primaryColor),
|
||||
titleTextStyle: AppStyle.headTitle2.copyWith(color: AppColor.writeColor),
|
||||
),
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: cardColor,
|
||||
contentTextStyle: AppStyle.title.copyWith(color: AppColor.writeColor),
|
||||
titleTextStyle: AppStyle.headTitle2.copyWith(color: AppColor.writeColor),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: cardColor,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
dividerTheme: DividerThemeData(
|
||||
color: dividerColor,
|
||||
thickness: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData lightThemeEnglish = _createTheme(
|
||||
brightness: Brightness.light,
|
||||
fontFamily: "SFPro",
|
||||
textTheme: TextTheme(
|
||||
displaySmall: AppStyle.title,
|
||||
displayLarge: AppStyle.headTitle,
|
||||
displayMedium: AppStyle.headTitle2,
|
||||
bodyLarge: AppStyle.title,
|
||||
bodyMedium: AppStyle.subtitle,
|
||||
),
|
||||
primarySwatch: Colors.blue,
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: AppColor.secondaryColor,
|
||||
contentTextStyle: AppStyle.title,
|
||||
titleTextStyle: AppStyle.headTitle2,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 0,
|
||||
color: AppColor.secondaryColor,
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
toolbarTextStyle: TextTheme(
|
||||
titleSmall: AppStyle.subtitle,
|
||||
headlineSmall: AppStyle.title,
|
||||
titleLarge: AppStyle.headTitle2,
|
||||
).bodyMedium,
|
||||
titleTextStyle: TextTheme(
|
||||
titleSmall: AppStyle.subtitle,
|
||||
headlineSmall: AppStyle.title,
|
||||
titleLarge: AppStyle.headTitle2,
|
||||
).titleLarge,
|
||||
),
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
cardColor: Colors.white,
|
||||
dividerColor: Colors.black12,
|
||||
);
|
||||
|
||||
ThemeData darkThemeEnglish = ThemeData(
|
||||
ThemeData darkThemeEnglish = _createTheme(
|
||||
brightness: Brightness.dark,
|
||||
fontFamily: "SFPro",
|
||||
textTheme: TextTheme(
|
||||
displaySmall: AppStyle.title,
|
||||
displayLarge: AppStyle.headTitle,
|
||||
displayMedium: AppStyle.headTitle2,
|
||||
bodyLarge: AppStyle.title,
|
||||
bodyMedium: AppStyle.subtitle,
|
||||
),
|
||||
primarySwatch: Colors.blue,
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: AppColor.secondaryColor,
|
||||
contentTextStyle: AppStyle.title,
|
||||
titleTextStyle: AppStyle.headTitle2,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 0,
|
||||
color: AppColor.secondaryColor,
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
toolbarTextStyle: TextTheme(
|
||||
titleSmall: AppStyle.subtitle,
|
||||
headlineSmall: AppStyle.title,
|
||||
titleLarge: AppStyle.headTitle2,
|
||||
).bodyMedium,
|
||||
titleTextStyle: TextTheme(
|
||||
titleSmall: AppStyle.subtitle,
|
||||
headlineSmall: AppStyle.title,
|
||||
titleLarge: AppStyle.headTitle2,
|
||||
).titleLarge,
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xFF121212),
|
||||
cardColor: const Color(0xFF1E1E1E),
|
||||
dividerColor: Colors.white10,
|
||||
);
|
||||
|
||||
ThemeData lightThemeArabic = ThemeData(
|
||||
ThemeData lightThemeArabic = _createTheme(
|
||||
brightness: Brightness.light,
|
||||
fontFamily: 'SFArabic',
|
||||
textTheme: TextTheme(
|
||||
displaySmall: AppStyle.title,
|
||||
displayLarge: AppStyle.headTitle,
|
||||
displayMedium: AppStyle.headTitle2,
|
||||
bodyLarge: AppStyle.title,
|
||||
bodyMedium: AppStyle.subtitle,
|
||||
),
|
||||
primarySwatch: Colors.blue,
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: AppColor.secondaryColor,
|
||||
contentTextStyle: AppStyle.title,
|
||||
titleTextStyle: AppStyle.headTitle2,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 0,
|
||||
color: AppColor.secondaryColor,
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
toolbarTextStyle: TextTheme(
|
||||
titleSmall: AppStyle.subtitle,
|
||||
headlineSmall: AppStyle.title,
|
||||
titleLarge: AppStyle.headTitle2,
|
||||
).bodyMedium,
|
||||
titleTextStyle: TextTheme(
|
||||
titleSmall: AppStyle.subtitle,
|
||||
headlineSmall: AppStyle.title,
|
||||
titleLarge: AppStyle.headTitle2,
|
||||
).titleLarge,
|
||||
),
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
cardColor: Colors.white,
|
||||
dividerColor: Colors.black12,
|
||||
);
|
||||
|
||||
ThemeData darkThemeArabic = ThemeData(
|
||||
ThemeData darkThemeArabic = _createTheme(
|
||||
brightness: Brightness.dark,
|
||||
fontFamily: 'SFArabic',
|
||||
textTheme: TextTheme(
|
||||
displaySmall: AppStyle.title,
|
||||
displayLarge: AppStyle.headTitle,
|
||||
displayMedium: AppStyle.headTitle2,
|
||||
bodyLarge: AppStyle.title,
|
||||
bodyMedium: AppStyle.subtitle,
|
||||
),
|
||||
primarySwatch: Colors.blue,
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: AppColor.secondaryColor,
|
||||
contentTextStyle: AppStyle.title,
|
||||
titleTextStyle: AppStyle.headTitle2,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 0,
|
||||
color: AppColor.secondaryColor,
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
toolbarTextStyle: TextTheme(
|
||||
titleSmall: AppStyle.subtitle,
|
||||
headlineSmall: AppStyle.title,
|
||||
titleLarge: AppStyle.headTitle2,
|
||||
).bodyMedium,
|
||||
titleTextStyle: TextTheme(
|
||||
titleSmall: AppStyle.subtitle,
|
||||
headlineSmall: AppStyle.title,
|
||||
titleLarge: AppStyle.headTitle2,
|
||||
).titleLarge,
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xFF121212),
|
||||
cardColor: const Color(0xFF1E1E1E),
|
||||
dividerColor: Colors.white10,
|
||||
);
|
||||
|
||||
@@ -67,7 +67,7 @@ class CompatibilityDetailCard extends StatelessWidget {
|
||||
color: Colors.grey.shade800,
|
||||
fontWeight: FontWeight.w600)),
|
||||
),
|
||||
Text("$achievedScore/$maxScore نقطة",
|
||||
Text("$achievedScore/$maxScore ${"points".tr}",
|
||||
style: TextStyle(
|
||||
color: color, fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
],
|
||||
@@ -130,10 +130,10 @@ class _DeviceCompatibilityPageState extends State<DeviceCompatibilityPage> {
|
||||
}
|
||||
|
||||
String _getScoreMessage(int value) {
|
||||
if (value >= 80) return "جهازك يقدم أداءً ممتازاً";
|
||||
if (value >= 60) return "جهازك جيد ومناسب جداً";
|
||||
if (value >= 40) return "متوافق، قد تلاحظ بعض البطء";
|
||||
return "قد لا يعمل التطبيق بالشكل الأمثل";
|
||||
if (value >= 80) return "Your device provides excellent performance".tr;
|
||||
if (value >= 60) return "Your device is good and very suitable".tr;
|
||||
if (value >= 40) return "Compatible, you may notice some slowness".tr;
|
||||
return "The app may not work optimally".tr;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -143,8 +143,8 @@ class _DeviceCompatibilityPageState extends State<DeviceCompatibilityPage> {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F8FC),
|
||||
appBar: AppBar(
|
||||
title: const Text("توافق الجهاز",
|
||||
style: TextStyle(
|
||||
title: Text("Device Compatibility".tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.black87, fontWeight: FontWeight.bold)),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
@@ -159,12 +159,12 @@ class _DeviceCompatibilityPageState extends State<DeviceCompatibilityPage> {
|
||||
children: [
|
||||
const Icon(Icons.phone_iphone, size: 56, color: Colors.grey),
|
||||
const SizedBox(height: 12),
|
||||
const Text("هذه الصفحة متاحة لأجهزة أندرويد فقط",
|
||||
Text("This page is only available for Android devices".tr,
|
||||
style: TextStyle(fontSize: 16)),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text("رجوع"),
|
||||
child: Text("Back".tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -176,9 +176,9 @@ class _DeviceCompatibilityPageState extends State<DeviceCompatibilityPage> {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F8FC),
|
||||
appBar: AppBar(
|
||||
title: const Text("توافق الجهاز",
|
||||
title: Text("Device Compatibility".tr,
|
||||
style:
|
||||
TextStyle(color: Colors.black87, fontWeight: FontWeight.bold)),
|
||||
const TextStyle(color: Colors.black87, fontWeight: FontWeight.bold)),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
@@ -208,7 +208,7 @@ class _DeviceCompatibilityPageState extends State<DeviceCompatibilityPage> {
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text("المتابعة إلى التطبيق",
|
||||
child: Text("Continue to App".tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
||||
3
lib/env/env.dart
vendored
3
lib/env/env.dart
vendored
@@ -12,6 +12,9 @@ abstract class Env {
|
||||
@EnviedField(varName: 'mapAPIKEYIOS', obfuscate: true)
|
||||
static final String mapAPIKEYIOS = _Env.mapAPIKEYIOS;
|
||||
|
||||
@EnviedField(varName: 'mapSaasKey', obfuscate: true)
|
||||
static final String mapSaasKey = _Env.mapSaasKey;
|
||||
|
||||
@EnviedField(varName: 'email', obfuscate: true)
|
||||
static final String email = _Env.email;
|
||||
|
||||
|
||||
24722
lib/env/env.g.dart
vendored
24722
lib/env/env.g.dart
vendored
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@ import 'print.dart';
|
||||
import 'splash_screen_page.dart';
|
||||
import 'views/home/Captin/orderCaptin/order_request_page.dart';
|
||||
import 'views/home/Captin/driver_map_page.dart';
|
||||
import 'controller/profile/setting_controller.dart';
|
||||
|
||||
final box = GetStorage();
|
||||
const storage = FlutterSecureStorage();
|
||||
@@ -315,10 +316,15 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
|
||||
Future<void> _initApp() async {
|
||||
try {
|
||||
if (!Get.isRegistered<NotificationController>())
|
||||
if (!Get.isRegistered<NotificationController>()) {
|
||||
Get.put(NotificationController());
|
||||
if (!Get.isRegistered<FirebaseMessagesController>())
|
||||
}
|
||||
if (!Get.isRegistered<FirebaseMessagesController>()) {
|
||||
Get.put(FirebaseMessagesController()).getToken();
|
||||
}
|
||||
if (!Get.isRegistered<SettingController>()) {
|
||||
Get.put(SettingController());
|
||||
}
|
||||
await FirebaseMessaging.instance.requestPermission();
|
||||
await NotificationController().initNotifications();
|
||||
|
||||
@@ -433,12 +439,15 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
|
||||
print("📥 Server Response: $res");
|
||||
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
// إغلاق أي ديالوج مفتوح باستخدام المفتاح العالمي لضمان الموثوقية
|
||||
if (Get.isDialogOpen == true) {
|
||||
navigatorKey.currentState?.pop();
|
||||
}
|
||||
|
||||
bool isFailure = false;
|
||||
if (res is Map && res['status'] == 'failure')
|
||||
if (res is Map && res['status'] == 'failure') {
|
||||
isFailure = true;
|
||||
else if (res == 'failure') isFailure = true;
|
||||
} else if (res == 'failure') isFailure = true;
|
||||
|
||||
if (isFailure) {
|
||||
Get.defaultDialog(
|
||||
@@ -465,7 +474,9 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
|
||||
Get.offAll(() => PassengerLocationMapPage(), arguments: rideArgs);
|
||||
} catch (e) {
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
if (Get.isDialogOpen == true) {
|
||||
navigatorKey.currentState?.pop();
|
||||
}
|
||||
print("❌ Error in accept process: $e");
|
||||
Get.snackbar("خطأ", "حدث خطأ غير متوقع");
|
||||
}
|
||||
@@ -511,13 +522,18 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final LocaleController localController = Get.put(LocaleController());
|
||||
final SettingController settingController = Get.put(SettingController());
|
||||
|
||||
return GetMaterialApp(
|
||||
navigatorKey: navigatorKey,
|
||||
title: AppInformation.appName,
|
||||
translations: MyTranslation(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: localController.language,
|
||||
theme: localController.appTheme,
|
||||
theme: localController.lightTheme,
|
||||
darkTheme: localController.darkTheme,
|
||||
themeMode: settingController.isDarkMode ? ThemeMode.dark : ThemeMode.light,
|
||||
|
||||
initialRoute: '/',
|
||||
getPages: [
|
||||
GetPage(name: '/', page: () => SplashScreen()),
|
||||
@@ -529,3 +545,4 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// lib/models/model/order_data.dart
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class OrderData {
|
||||
final String customerName;
|
||||
final String customerToken;
|
||||
@@ -122,17 +124,17 @@ class OrderData {
|
||||
static String _getRideType(String type) {
|
||||
switch (type) {
|
||||
case 'Comfort':
|
||||
return 'كمفورت ❄️';
|
||||
return 'Comfort ❄️'.tr;
|
||||
case 'Lady':
|
||||
return 'ليدي 👩';
|
||||
return 'Lady 👩'.tr;
|
||||
case 'Speed':
|
||||
return 'سبيد 🔻';
|
||||
return 'Speed 🔻'.tr;
|
||||
case 'Mashwari':
|
||||
return 'مشواري';
|
||||
return 'Mashwari'.tr;
|
||||
case 'Rayeh Gai':
|
||||
return 'رايح جاي';
|
||||
return 'Rayeh Gai'.tr;
|
||||
default:
|
||||
return type;
|
||||
return type.tr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ class Log {
|
||||
Log._();
|
||||
|
||||
static void print(String value, {StackTrace? stackTrace}) {
|
||||
developer.log(value, name: 'LOG', stackTrace: stackTrace);
|
||||
// developer.log(value, name: 'LOG', stackTrace: stackTrace);
|
||||
}
|
||||
|
||||
static Object? inspect(Object? object) {
|
||||
|
||||
119
lib/services/offline_map_service.dart
Normal file
119
lib/services/offline_map_service.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'dart:math' as math;
|
||||
import '../main.dart';
|
||||
import '../print.dart';
|
||||
|
||||
class OfflineMapService {
|
||||
static final OfflineMapService instance = OfflineMapService._();
|
||||
OfflineMapService._();
|
||||
|
||||
final _offlineRegionName = "UserRegion";
|
||||
bool _isDownloading = false;
|
||||
LatLng? _lastDownloadedCenter;
|
||||
|
||||
/// Calculate bounding box for a given center and radius in km
|
||||
LatLngBounds _calculateBounds(LatLng center, double radiusKm) {
|
||||
const double earthRadius = 6371.0;
|
||||
|
||||
// Latitude degrees per km
|
||||
double latDelta = (radiusKm / earthRadius) * (180 / math.pi);
|
||||
// Longitude degrees per km at given latitude
|
||||
double lngDelta = (radiusKm / earthRadius) *
|
||||
(180 / math.pi) /
|
||||
math.cos(center.latitude * math.pi / 180);
|
||||
|
||||
return LatLngBounds(
|
||||
southwest:
|
||||
LatLng(center.latitude - latDelta, center.longitude - lngDelta),
|
||||
northeast:
|
||||
LatLng(center.latitude + latDelta, center.longitude + lngDelta),
|
||||
);
|
||||
}
|
||||
|
||||
/// Downloads a specified radius around a coordinate
|
||||
Future<void> downloadRegion(LatLng center,
|
||||
{double radiusKm = 10.0,
|
||||
double minZoom = 6.0,
|
||||
double maxZoom = 15.0}) async {
|
||||
if (_isDownloading) return;
|
||||
|
||||
// Avoid re-downloading if the user hasn't moved significantly (e.g. > 5km)
|
||||
if (_lastDownloadedCenter != null) {
|
||||
double distance = _calculateDistance(center, _lastDownloadedCenter!);
|
||||
if (distance < 5.0) return; // skip if close to previously downloaded
|
||||
}
|
||||
|
||||
_isDownloading = true;
|
||||
|
||||
try {
|
||||
final bounds = _calculateBounds(center, radiusKm);
|
||||
|
||||
// Select style based on current theme
|
||||
final String styleStr =
|
||||
Get.isDarkMode ? "assets/style_dark.json" : "assets/style.json";
|
||||
|
||||
// iOS native crash guard: MLNTilePyramidOfflineRegion does not support relative asset URLs.
|
||||
// We skip native offline registration on iOS if using local assets to ensure stability.
|
||||
if (Platform.isIOS && !styleStr.startsWith('http')) {
|
||||
Log.print(
|
||||
"ℹ️ Skipping native offline registration on iOS for asset-based style to prevent crash.");
|
||||
return;
|
||||
}
|
||||
|
||||
final regionDefinition = OfflineRegionDefinition(
|
||||
bounds: bounds,
|
||||
mapStyleUrl: styleStr,
|
||||
minZoom: minZoom,
|
||||
maxZoom: maxZoom,
|
||||
);
|
||||
|
||||
// We'll update the last downloaded center immediately
|
||||
_lastDownloadedCenter = center;
|
||||
|
||||
// MapLibre standard API for offline downloads
|
||||
await downloadOfflineRegion(regionDefinition, metadata: {
|
||||
'name': '$_offlineRegionName-${center.latitude}-${center.longitude}',
|
||||
'downloadDate': DateTime.now().toIso8601String(),
|
||||
});
|
||||
|
||||
// Reassurance log for the user
|
||||
Log.print("📍 Map Ready: Service is utilizing local tile cache.");
|
||||
Log.print(
|
||||
"✅ Offline Map Cached for Region: $center (radius: ${radiusKm}km, style: $styleStr)");
|
||||
} catch (e) {
|
||||
Log.print("⚠️ Offline Map Download Failed: $e");
|
||||
} finally {
|
||||
_isDownloading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to calculate distance in km
|
||||
double _calculateDistance(LatLng p1, LatLng p2) {
|
||||
var p = 0.017453292519943295;
|
||||
var c = math.cos;
|
||||
var a = 0.5 -
|
||||
c((p2.latitude - p1.latitude) * p) / 2 +
|
||||
c(p1.latitude * p) *
|
||||
c(p2.latitude * p) *
|
||||
(1 - c((p2.longitude - p1.longitude) * p)) /
|
||||
2;
|
||||
return 12742 * math.asin(math.sqrt(a));
|
||||
}
|
||||
|
||||
/// Clears all offline map regions and tiles from local storage
|
||||
Future<void> clearCache() async {
|
||||
try {
|
||||
Log.print("♻️ Purging MapLibre Offline Cache...");
|
||||
final List<OfflineRegion> regions = await getListOfRegions();
|
||||
for (var region in regions) {
|
||||
await deleteOfflineRegion(region.id);
|
||||
}
|
||||
Log.print("✅ Map cache cleared successfully. ${regions.length} regions removed.");
|
||||
} catch (e) {
|
||||
Log.print("⚠️ Failed to clear map cache: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -749,5 +749,58 @@
|
||||
"My Profile": "",
|
||||
"Female": "",
|
||||
"Update Available": "",
|
||||
"Recorded Trips for Safety": ""
|
||||
"Recorded Trips for Safety": "",
|
||||
"Available Balance": "الرصيد المتاح",
|
||||
"Available Balance Ar": "Available Balance",
|
||||
"Last updated:": "آخر تحديث:",
|
||||
"Just now": "الآن",
|
||||
"Add Balance": "شحن الرصيد",
|
||||
"Withdraw": "سحب",
|
||||
"Transfer": "تحويل",
|
||||
"History": "السجل",
|
||||
"Earnings Summary": "ملخص الأرباح",
|
||||
"Wallet Details": "تفاصيل المحفظة",
|
||||
"Progress:": "التقدم:",
|
||||
"Claim Reward": "الحصول على المكافأة",
|
||||
"Today": "اليوم",
|
||||
"Yesterday": "أمس",
|
||||
"This Week": "هذا الأسبوع",
|
||||
"Trip Payment": "دفع رحلة",
|
||||
"Total Earnings": "إجمالي الأرباح",
|
||||
"Total Trips": "إجمالي الرحلات",
|
||||
"Commission": "العمولة",
|
||||
"Net Profit": "صافي الربح",
|
||||
"Cash Earnings": "أرباح نقداً",
|
||||
"Card Earnings": "أرباح البطاقة",
|
||||
"Pending": "قيد الانتظار",
|
||||
"View All": "عرض الكل",
|
||||
"Last updated: before 2 min": "آخر تحديث: قبل دقيقتين",
|
||||
"PTS": "نقطة",
|
||||
"E-Cash payment gateway": "بوابة دفع E-Cash",
|
||||
"Syriatel Cash": "سيريتل كاش",
|
||||
"Pay using Syriatel mobile wallet": "الدفع عبر محفظة سيريتل",
|
||||
"Sham Cash": "شام كاش",
|
||||
"Pay using Sham Cash wallet": "الدفع عبر محفظة شام كاش",
|
||||
"Confirm payment with biometrics": "تأكيد الدفع بالبصمة",
|
||||
"Wallet Phone Number": "رقم هاتف المحفظة",
|
||||
"Confirm": "تأكيد",
|
||||
"Buy Points Packages": "باقات شحن الرصيد",
|
||||
"Select Payment Method": "اختر طريقة الدفع",
|
||||
"You are buying": "أنت تقوم بشراء",
|
||||
"Points": "ل.س",
|
||||
"for": "بـ",
|
||||
"Use Touch ID or Face ID to confirm payment": "استخدم بصمة الإصبع أو الوجه لتأكيد الدفع",
|
||||
"Your Budget less than needed": "رصيدك أقل من المطلوب",
|
||||
"Pay from my budget": "الدفع من الرصيد المتاح",
|
||||
"Select how you want to charge your account": "اختر طريقة شحن حسابك",
|
||||
"Pay": "دفع",
|
||||
"Cancel": "إلغاء",
|
||||
"from 7:00am to 10:00am": "من 7:00 صباحاً حتى 10:00 صباحاً",
|
||||
"from 3:00pm to 6:00 pm": "من 3:00 مساءً حتى 6:00 مساءً",
|
||||
"Morning Promo": "عرض الصباح",
|
||||
"Afternoon Promo": "عرض بعد الظهر",
|
||||
"Recharge Balance": "شحن الرصيد",
|
||||
"Recharge Balance Packages": "باقات شحن الرصيد",
|
||||
"Price:": "السعر:",
|
||||
"Amount to charge:": "المبلغ المطلوب شحنه:"
|
||||
}
|
||||
@@ -749,5 +749,58 @@
|
||||
"My Profile": "",
|
||||
"Female": "",
|
||||
"Update Available": "",
|
||||
"Recorded Trips for Safety": ""
|
||||
"Recorded Trips for Safety": "",
|
||||
"Available Balance": "Available Balance",
|
||||
"Available Balance Ar": "الرصيد المتاح",
|
||||
"Last updated:": "Last updated:",
|
||||
"Just now": "Just now",
|
||||
"Add Balance": "Add Balance",
|
||||
"Withdraw": "Withdraw",
|
||||
"Transfer": "Transfer",
|
||||
"History": "History",
|
||||
"Earnings Summary": "Earnings Summary",
|
||||
"Wallet Details": "Wallet Details",
|
||||
"Progress:": "Progress:",
|
||||
"Claim Reward": "Claim Reward",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"This Week": "This Week",
|
||||
"Trip Payment": "Trip Payment",
|
||||
"Total Earnings": "Total Earnings",
|
||||
"Total Trips": "Total Trips",
|
||||
"Commission": "Commission",
|
||||
"Net Profit": "Net Profit",
|
||||
"Cash Earnings": "Cash Earnings",
|
||||
"Card Earnings": "Card Earnings",
|
||||
"Pending": "Pending",
|
||||
"View All": "View All",
|
||||
"Last updated: before 2 min": "Last updated: before 2 min",
|
||||
"PTS": "PTS",
|
||||
"E-Cash payment gateway": "E-Cash payment gateway",
|
||||
"Syriatel Cash": "Syriatel Cash",
|
||||
"Pay using Syriatel mobile wallet": "Pay using Syriatel mobile wallet",
|
||||
"Sham Cash": "Sham Cash",
|
||||
"Pay using Sham Cash wallet": "Pay using Sham Cash wallet",
|
||||
"Confirm payment with biometrics": "Confirm payment with biometrics",
|
||||
"Wallet Phone Number": "Wallet Phone Number",
|
||||
"Confirm": "Confirm",
|
||||
"Buy Points Packages": "Recharge Balance Packages",
|
||||
"Select Payment Method": "Select Payment Method",
|
||||
"You are buying": "You are buying",
|
||||
"Points": "SYP",
|
||||
"for": "for",
|
||||
"Use Touch ID or Face ID to confirm payment": "Use Touch ID or Face ID to confirm payment",
|
||||
"Your Budget less than needed": "Your Budget less than needed",
|
||||
"Pay from my budget": "Pay from my budget",
|
||||
"Select how you want to charge your account": "Select how you want to charge your account",
|
||||
"Pay": "Pay",
|
||||
"Cancel": "Cancel",
|
||||
"from 7:00am to 10:00am": "from 7:00am to 10:00am",
|
||||
"from 3:00pm to 6:00 pm": "from 3:00pm to 6:00 pm",
|
||||
"Morning Promo": "Morning Promo",
|
||||
"Afternoon Promo": "Afternoon Promo",
|
||||
"Recharge Balance": "Recharge Balance",
|
||||
"Recharge Balance Packages": "Recharge Balance Packages",
|
||||
"Price:": "Price:",
|
||||
"Amount to charge:": "Amount to charge:"
|
||||
}
|
||||
@@ -21,13 +21,11 @@ class RatePassenger extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Trip Completed'.tr,
|
||||
style: const TextStyle(color: Colors.black)),
|
||||
title: Text('Trip Completed'.tr),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
),
|
||||
body: GetBuilder<RateController>(
|
||||
@@ -156,23 +154,23 @@ class RatePassenger extends StatelessWidget {
|
||||
Widget _buildWalletSection(BuildContext context, RateController controller) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: controller.ispassengerWantWalletFromDriver
|
||||
? _buildAmountInput(controller)
|
||||
: _buildWalletQuery(controller),
|
||||
? _buildAmountInput(context, controller)
|
||||
: _buildWalletQuery(context, controller),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWalletQuery(RateController controller) {
|
||||
Widget _buildWalletQuery(BuildContext context, RateController controller) {
|
||||
return Column(
|
||||
key: const ValueKey('walletQuery'),
|
||||
children: [
|
||||
@@ -181,11 +179,11 @@ class RatePassenger extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
color: Colors.amber.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.account_balance_wallet,
|
||||
color: Colors.orange),
|
||||
child:
|
||||
const Icon(Icons.account_balance_wallet, color: Colors.amber),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
@@ -206,8 +204,8 @@ class RatePassenger extends StatelessWidget {
|
||||
child: OutlinedButton(
|
||||
onPressed: () {}, // Optional logic
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.grey,
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
foregroundColor: Theme.of(context).hintColor,
|
||||
side: BorderSide(color: Theme.of(context).dividerColor),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
@@ -235,14 +233,17 @@ class RatePassenger extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAmountInput(RateController controller) {
|
||||
Widget _buildAmountInput(BuildContext context, RateController controller) {
|
||||
return Column(
|
||||
key: const ValueKey('amountInput'),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Enter Amount Paid".tr,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Form(
|
||||
@@ -279,11 +280,12 @@ class RatePassenger extends StatelessWidget {
|
||||
Text(
|
||||
'Rate Passenger'.tr,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
RatingBar.builder(
|
||||
initialRating: 0,
|
||||
@@ -306,19 +308,20 @@ class RatePassenger extends StatelessWidget {
|
||||
TextField(
|
||||
controller: controller.comment,
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Any comments about the passenger?'.tr,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
borderSide: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade200),
|
||||
borderSide: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,83 +1,83 @@
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||
// import 'package:camera/camera.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../constant/style.dart';
|
||||
import '../../../../controller/functions/camer_controller.dart';
|
||||
import '../../../../controller/functions/ocr_controller.dart';
|
||||
import '../../../widgets/my_scafold.dart';
|
||||
// import '../../../../constant/colors.dart';
|
||||
// import '../../../../constant/style.dart';
|
||||
// import '../../../../controller/functions/camer_controller.dart';
|
||||
// import '../../../../controller/functions/ocr_controller.dart';
|
||||
// import '../../../widgets/my_scafold.dart';
|
||||
|
||||
class CameraLisencePage extends StatelessWidget {
|
||||
CameraLisencePage.CameraLicensePage({super.key});
|
||||
final CameraClassController cameraClassController =
|
||||
Get.put(CameraClassController());
|
||||
// class CameraLisencePage extends StatelessWidget {
|
||||
// CameraLisencePage.CameraLicensePage({super.key});
|
||||
// final CameraClassController cameraClassController =
|
||||
// Get.put(CameraClassController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'Scan Driver License'.tr,
|
||||
body: [
|
||||
Column(children: [
|
||||
Text(
|
||||
'Please put your licence in these border'.tr,
|
||||
style: AppStyle.title.copyWith(color: AppColor.greenColor),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: GetBuilder<CameraClassController>(
|
||||
builder: (cameraClassController) =>
|
||||
cameraClassController.isCameraInitialized
|
||||
? Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
child: CameraPreview(
|
||||
cameraClassController.cameraController,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: Get.height * .1,
|
||||
right: 5,
|
||||
left: 5,
|
||||
child: Container(
|
||||
height: Get.width * 3 / 4,
|
||||
width: Get.width * .9,
|
||||
decoration: BoxDecoration(
|
||||
// color: AppColor.blueColor,
|
||||
border: Border.all(
|
||||
color: AppColor.yellowColor, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
height: Get.width * 3 / 4,
|
||||
width: Get.width,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Camera not initialized yet',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
MyElevatedButton(
|
||||
title: 'Take Image'.tr,
|
||||
onPressed: () {
|
||||
ScanDocumentsByApi().scanDocumentsByApi();
|
||||
},
|
||||
)
|
||||
]),
|
||||
],
|
||||
isleading: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return MyScafolld(
|
||||
// title: 'Scan Driver License'.tr,
|
||||
// body: [
|
||||
// Column(children: [
|
||||
// Text(
|
||||
// 'Please put your licence in these border'.tr,
|
||||
// style: AppStyle.title.copyWith(color: AppColor.greenColor),
|
||||
// ),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
// child: GetBuilder<CameraClassController>(
|
||||
// builder: (cameraClassController) =>
|
||||
// cameraClassController.isCameraInitialized
|
||||
// ? Stack(
|
||||
// children: [
|
||||
// Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// child: CameraPreview(
|
||||
// cameraClassController.cameraController,
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: Get.height * .1,
|
||||
// right: 5,
|
||||
// left: 5,
|
||||
// child: Container(
|
||||
// height: Get.width * 3 / 4,
|
||||
// width: Get.width * .9,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.yellowColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// )
|
||||
// : Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// height: Get.width * 3 / 4,
|
||||
// width: Get.width,
|
||||
// child: Center(
|
||||
// child: Text(
|
||||
// 'Camera not initialized yet',
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 20,
|
||||
// ),
|
||||
// MyElevatedButton(
|
||||
// title: 'Take Image'.tr,
|
||||
// onPressed: () {
|
||||
// ScanDocumentsByApi().scanDocumentsByApi();
|
||||
// },
|
||||
// )
|
||||
// ]),
|
||||
// ],
|
||||
// isleading: true,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -61,7 +61,7 @@ class _LoginCaptinState extends State<LoginCaptin> with WidgetsBindingObserver {
|
||||
return GetBuilder<LoginDriverController>(
|
||||
builder: (controller) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.secondaryColor,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: _buildBodyContent(context, controller),
|
||||
@@ -72,6 +72,7 @@ class _LoginCaptinState extends State<LoginCaptin> with WidgetsBindingObserver {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildBodyContent(
|
||||
BuildContext context, LoginDriverController controller) {
|
||||
// 1. صفحة الموافقة على الشروط
|
||||
@@ -130,17 +131,22 @@ class _LoginCaptinState extends State<LoginCaptin> with WidgetsBindingObserver {
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).cardColor.withOpacity(0.5),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: HtmlWidget(box.read(BoxName.lang).toString() == 'ar'
|
||||
? AppInformation.privacyPolicyArabic
|
||||
: AppInformation.privacyPolicy),
|
||||
child: HtmlWidget(
|
||||
box.read(BoxName.lang).toString() == 'ar'
|
||||
? AppInformation.privacyPolicyArabic
|
||||
: AppInformation.privacyPolicy,
|
||||
textStyle: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
CheckboxListTile(
|
||||
title: Text('I Agree'.tr, style: AppStyle.title),
|
||||
value: controller.isAgreeTerms,
|
||||
@@ -324,14 +330,15 @@ class _LoginCaptinState extends State<LoginCaptin> with WidgetsBindingObserver {
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(child: Divider()),
|
||||
Expanded(child: Divider(color: Theme.of(context).dividerColor)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text('Or'.tr, style: AppStyle.subtitle),
|
||||
),
|
||||
const Expanded(child: Divider()),
|
||||
Expanded(child: Divider(color: Theme.of(context).dividerColor)),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
MyElevatedButton(
|
||||
title: 'Create Account with Email'.tr,
|
||||
@@ -457,19 +464,29 @@ class _LoginCaptinState extends State<LoginCaptin> with WidgetsBindingObserver {
|
||||
validator: validator,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText,
|
||||
labelStyle: TextStyle(color: Theme.of(context).hintColor),
|
||||
hintText: hintText,
|
||||
hintStyle: TextStyle(color: Theme.of(context).hintColor.withOpacity(0.5)),
|
||||
prefixIcon: Icon(prefixIcon, color: AppColor.primaryColor),
|
||||
suffixIcon: suffixIcon,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.0)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderSide: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderSide: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderSide:
|
||||
const BorderSide(color: AppColor.primaryColor, width: 2.0),
|
||||
borderSide: const BorderSide(color: AppColor.primaryColor, width: 2.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
Widget _buildSocialButton({
|
||||
|
||||
@@ -23,10 +23,10 @@ class RegisterPage extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: Offset(3, 3),
|
||||
offset: const Offset(3, 3),
|
||||
color: AppColor.accentColor,
|
||||
blurRadius: 3)
|
||||
],
|
||||
@@ -45,7 +45,7 @@ class RegisterPage extends StatelessWidget {
|
||||
controller: controller.firstNameController,
|
||||
decoration: InputDecoration(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(
|
||||
borderSide: BorderSide(
|
||||
color: AppColor.primaryColor,
|
||||
width: 2.0,
|
||||
),
|
||||
@@ -54,7 +54,7 @@ class RegisterPage extends StatelessWidget {
|
||||
fillColor: AppColor.accentColor,
|
||||
hoverColor: AppColor.accentColor,
|
||||
focusColor: AppColor.accentColor,
|
||||
border: const OutlineInputBorder(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12))),
|
||||
labelText: 'First name'.tr,
|
||||
@@ -75,7 +75,7 @@ class RegisterPage extends StatelessWidget {
|
||||
controller: controller.lastNameController,
|
||||
decoration: InputDecoration(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(
|
||||
borderSide: BorderSide(
|
||||
color: AppColor.primaryColor,
|
||||
width: 2.0,
|
||||
),
|
||||
@@ -83,7 +83,7 @@ class RegisterPage extends StatelessWidget {
|
||||
),
|
||||
focusColor: AppColor.accentColor,
|
||||
fillColor: AppColor.accentColor,
|
||||
border: const OutlineInputBorder(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12))),
|
||||
labelText: 'Last name'.tr,
|
||||
@@ -116,7 +116,7 @@ class RegisterPage extends StatelessWidget {
|
||||
fillColor: AppColor.accentColor,
|
||||
hoverColor: AppColor.accentColor,
|
||||
focusColor: AppColor.accentColor,
|
||||
border: const OutlineInputBorder(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(12))),
|
||||
labelText: 'Email'.tr,
|
||||
@@ -149,7 +149,7 @@ class RegisterPage extends StatelessWidget {
|
||||
fillColor: AppColor.accentColor,
|
||||
hoverColor: AppColor.accentColor,
|
||||
focusColor: AppColor.accentColor,
|
||||
border: const OutlineInputBorder(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(12))),
|
||||
labelText: 'Password'.tr,
|
||||
@@ -188,7 +188,7 @@ class RegisterPage extends StatelessWidget {
|
||||
),
|
||||
focusColor: AppColor.accentColor,
|
||||
fillColor: AppColor.accentColor,
|
||||
border: const OutlineInputBorder(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12))),
|
||||
labelText: 'Phone'.tr,
|
||||
@@ -217,7 +217,7 @@ class RegisterPage extends StatelessWidget {
|
||||
),
|
||||
focusColor: AppColor.accentColor,
|
||||
fillColor: AppColor.accentColor,
|
||||
border: const OutlineInputBorder(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12))),
|
||||
labelText: 'City'.tr,
|
||||
|
||||
@@ -59,6 +59,14 @@ class SettingsCaptain extends StatelessWidget {
|
||||
_buildSectionHeader('App Preferences'.tr, context),
|
||||
_buildSettingsCard(
|
||||
children: [
|
||||
_buildSwitchTile(
|
||||
icon: Icons.dark_mode_outlined,
|
||||
title: 'App Dark Mode'.tr,
|
||||
subtitle: 'Switch between light and dark themes'.tr,
|
||||
controller: settingsController,
|
||||
valueGetter: (ctrl) => (ctrl).isDarkMode,
|
||||
onChanged: (ctrl) => (ctrl).toggleAppTheme(),
|
||||
),
|
||||
_buildSwitchTile(
|
||||
icon: Icons.map_outlined,
|
||||
color: AppColor.redColor,
|
||||
@@ -68,6 +76,15 @@ class SettingsCaptain extends StatelessWidget {
|
||||
valueGetter: (ctrl) => (ctrl).isGoogleMapsEnabled,
|
||||
onChanged: (ctrl) => (ctrl).onChangMapApp(),
|
||||
),
|
||||
_buildSwitchTile(
|
||||
icon: Icons.map_outlined,
|
||||
title: 'Map Dark Mode'.tr,
|
||||
subtitle: 'Switch between light and dark map styles'.tr,
|
||||
controller: settingsController,
|
||||
valueGetter: (ctrl) => (ctrl).isMapDarkMode,
|
||||
onChanged: (ctrl) => (ctrl).toggleMapTheme(),
|
||||
),
|
||||
|
||||
_buildSwitchTile(
|
||||
icon: Icons.vibration,
|
||||
title: 'Vibration'.tr,
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/main.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -9,208 +6,171 @@ import '../../../controller/home/captin/help/assurance_controller.dart';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../widgets/my_scafold.dart';
|
||||
|
||||
class AssuranceHealthPage extends StatelessWidget {
|
||||
AssuranceHealthPage({super.key});
|
||||
AssuranceHealthController assuranceHealthController =
|
||||
final AssuranceHealthController assuranceHealthController =
|
||||
Get.put(AssuranceHealthController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text("Health Insurance".tr),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GetBuilder<AssuranceHealthController>(
|
||||
builder: (assuranceHealthController) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
"When you complete 500 trips, you will be eligible for exclusive health insurance offers."
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
CupertinoButton.filled(
|
||||
child: Text("Show My Trip Count".tr),
|
||||
onPressed: () async {
|
||||
assuranceHealthController.getTripCountByCaptain();
|
||||
},
|
||||
final theme = Theme.of(context);
|
||||
return MyScafolld(
|
||||
title: "Health Insurance".tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
GetBuilder<AssuranceHealthController>(
|
||||
builder: (controller) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
"When you complete 500 trips, you will be eligible for exclusive health insurance offers."
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color:
|
||||
theme.textTheme.bodyMedium?.color?.withOpacity(0.8),
|
||||
height: 1.5,
|
||||
),
|
||||
_buildTripCountAvatar(
|
||||
assuranceHealthController.tripCount['count'] == null
|
||||
? '0'
|
||||
: assuranceHealthController.tripCount['count']
|
||||
.toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.systemGrey6,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
const SizedBox(height: 24),
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () =>
|
||||
controller.getTripCountByCaptain(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text("Show My Trip Count".tr),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildTripCountAvatar(
|
||||
controller.tripCount['count'] == null
|
||||
? '0'
|
||||
: controller.tripCount['count'].toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: theme.dividerColor),
|
||||
),
|
||||
child: Text(
|
||||
"We have partnered with health insurance providers to offer you special health coverage. Complete 500 trips and receive a 20% discount on health insurance premiums."
|
||||
.tr,
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.textStyle
|
||||
.copyWith(fontWeight: FontWeight.w600),
|
||||
style: theme.textTheme.bodyLarge
|
||||
?.copyWith(fontWeight: FontWeight.w500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
CupertinoButton.filled(
|
||||
disabledColor: AppColor.blueColor,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Text(
|
||||
"Would you like to proceed with health insurance?".tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.bold),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
onPressed: () => _showInsuranceDialog(context, controller),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text(
|
||||
"Would you like to proceed with health insurance?".tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
// Show confirmation dialog before proceeding
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
// Variable to store the health insurance provider chosen by the driver
|
||||
TextEditingController providerController =
|
||||
TextEditingController();
|
||||
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(
|
||||
"Confirmation".tr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 18),
|
||||
),
|
||||
content: Column(
|
||||
children: [
|
||||
Text(
|
||||
"Would you like to proceed with health insurance?"
|
||||
.tr,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
CupertinoTextField(
|
||||
controller: providerController,
|
||||
placeholder:
|
||||
"Do you have a disease for a long time?".tr,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: CupertinoColors.systemGrey,
|
||||
width: 1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
CupertinoDialogAction(
|
||||
isDefaultAction: true,
|
||||
child: Text(
|
||||
"Yes".tr,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.activeBlue),
|
||||
),
|
||||
onPressed: () async {
|
||||
// Ensure the provider name is not empty
|
||||
if (providerController.text.isNotEmpty) {
|
||||
// Call the function to insert data into the database
|
||||
await assuranceHealthController
|
||||
.addDriverHealthAssurance(
|
||||
healthInsuranceProvider:
|
||||
providerController.text,
|
||||
);
|
||||
|
||||
// Close the dialog and navigate to a success screen or show a snackbar
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
// Show an alert if the provider name is empty
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (_) => CupertinoAlertDialog(
|
||||
title: Text("Error".tr),
|
||||
content: Text(
|
||||
"Do you have a disease for a long time?"
|
||||
.tr),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text("OK".tr),
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: Text(
|
||||
"No".tr,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.destructiveRed),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.pop(); // Just close the dialog
|
||||
// Optionally show feedback if the driver opts out
|
||||
// Get.snackbar(
|
||||
// "Opted out".tr,
|
||||
// "You have chosen not to proceed with health insurance."
|
||||
// .tr,
|
||||
// backgroundColor:
|
||||
// CupertinoColors.systemGrey);
|
||||
mySnackeBarError(
|
||||
"You have chosen not to proceed with health insurance."
|
||||
.tr);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showInsuranceDialog(
|
||||
BuildContext context, AssuranceHealthController controller) {
|
||||
final TextEditingController providerController = TextEditingController();
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: Text("Confirmation".tr),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Would you like to proceed with health insurance?".tr),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: providerController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Do you have a disease for a long time?".tr,
|
||||
border:
|
||||
OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text("No".tr, style: const TextStyle(color: Colors.red)),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (providerController.text.isNotEmpty) {
|
||||
await controller.addDriverHealthAssurance(
|
||||
healthInsuranceProvider: providerController.text,
|
||||
);
|
||||
Get.back();
|
||||
} else {
|
||||
Get.snackbar("Error".tr,
|
||||
"Please provide details about any long-term diseases.".tr);
|
||||
}
|
||||
},
|
||||
child: Text("Yes".tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTripCountAvatar(String count) {
|
||||
return Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: const RadialGradient(
|
||||
colors: [
|
||||
Color(0xFF42A5F5),
|
||||
Color(0xFF1976D2),
|
||||
], // Health theme colors
|
||||
center: Alignment.center,
|
||||
radius: 0.8,
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF42A5F5), Color(0xFF1976D2)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: CupertinoColors.black.withOpacity(0.2),
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -220,9 +180,9 @@ class AssuranceHealthPage extends StatelessWidget {
|
||||
child: Text(
|
||||
count,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CupertinoColors.white,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -31,40 +31,32 @@ class HomeScreen extends StatelessWidget {
|
||||
],
|
||||
)),
|
||||
bottomNavigationBar: Obx(() => BottomNavigationBar(
|
||||
backgroundColor: Colors.greenAccent,
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
currentIndex: controller.currentIndex.value,
|
||||
onTap: controller.changePage,
|
||||
selectedItemColor: AppColor.primaryColor,
|
||||
unselectedItemColor: Theme.of(context).hintColor,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(
|
||||
Icons.home,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
icon: const Icon(Icons.home),
|
||||
label: 'Home'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(
|
||||
Icons.person,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
icon: const Icon(Icons.person),
|
||||
label: 'Profile'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(
|
||||
Icons.bar_chart,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
icon: const Icon(Icons.bar_chart),
|
||||
label: 'Statistics'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(
|
||||
Icons.account_balance_wallet,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
icon: const Icon(Icons.account_balance_wallet),
|
||||
label: 'Wallet'.tr,
|
||||
),
|
||||
],
|
||||
)),
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,303 +1,302 @@
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/constant/colors.dart';
|
||||
import 'package:sefer_driver/constant/style.dart';
|
||||
import 'package:sefer_driver/controller/functions/camer_controller.dart';
|
||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:sefer_driver/views/widgets/my_scafold.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:sefer_driver/constant/colors.dart';
|
||||
// import 'package:sefer_driver/constant/style.dart';
|
||||
// import 'package:sefer_driver/controller/functions/camer_controller.dart';
|
||||
// import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||
// import 'package:sefer_driver/views/widgets/my_scafold.dart';
|
||||
|
||||
class CameraWidgetCardId extends StatelessWidget {
|
||||
final CameraClassController cameraClassController =
|
||||
Get.put(CameraClassController());
|
||||
// class CameraWidgetCardId extends StatelessWidget {
|
||||
// final CameraClassController cameraClassController =
|
||||
// Get.put(CameraClassController());
|
||||
|
||||
CameraWidgetCardId({super.key});
|
||||
// CameraWidgetCardId({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'Scan Id'.tr,
|
||||
body: [
|
||||
Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: GetBuilder<CameraClassController>(
|
||||
builder: (cameraClassController) =>
|
||||
cameraClassController.isCameraInitialized
|
||||
? Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fitWidth,
|
||||
child: SizedBox(
|
||||
width: Get.width * .9,
|
||||
height: Get.width *
|
||||
.9, // Set the desired aspect ratio here
|
||||
child: CameraPreview(
|
||||
cameraClassController.cameraController,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 72,
|
||||
child: Container(
|
||||
width: 230,
|
||||
height: 25,
|
||||
decoration: BoxDecoration(
|
||||
// color: AppColor.blueColor,
|
||||
border: Border.all(
|
||||
color: AppColor.yellowColor,
|
||||
width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 60,
|
||||
right: 5,
|
||||
child: Container(
|
||||
width: 230,
|
||||
height: 25,
|
||||
decoration: BoxDecoration(
|
||||
// color: AppColor.blueColor,
|
||||
border: Border.all(
|
||||
color: AppColor.blueColor, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 156,
|
||||
right: 5,
|
||||
child: Container(
|
||||
width: 140,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
// color: AppColor.blueColor,
|
||||
border: Border.all(
|
||||
color: AppColor.blueColor, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 175,
|
||||
right: 5,
|
||||
child: Container(
|
||||
width: 140,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
// color: AppColor.blueColor,
|
||||
border: Border.all(
|
||||
color: AppColor.blueColor, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 191,
|
||||
right: 5,
|
||||
child: Container(
|
||||
width: 140,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
// color: AppColor.blueColor,
|
||||
border: Border.all(
|
||||
color: AppColor.blueColor, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 207,
|
||||
right: 5,
|
||||
child: Container(
|
||||
width: 140,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
// color: AppColor.blueColor,
|
||||
border: Border.all(
|
||||
color: AppColor.blueColor, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 225,
|
||||
right: 5,
|
||||
child: Container(
|
||||
width: 140,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
// color: AppColor.blueColor,
|
||||
border: Border.all(
|
||||
color: AppColor.blueColor, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 115,
|
||||
left: 25,
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 110,
|
||||
decoration: BoxDecoration(
|
||||
// color: AppColor.blueColor,
|
||||
border: Border.all(
|
||||
color: AppColor.blueColor, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
height: Get.width * 3 / 4,
|
||||
width: Get.width,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Camera not initilaized yet'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
title: 'Scan ID MklGoogle'.tr, onPressed: () {}),
|
||||
// cameraClassController.takePictureAndMLGoogleScan()),
|
||||
MyElevatedButton(
|
||||
title: 'Scan ID Tesseract'.tr, onPressed: () {}),
|
||||
],
|
||||
),
|
||||
MyElevatedButton(
|
||||
title: 'Scan ID Api'.tr,
|
||||
onPressed: () => cameraClassController.extractCardId()),
|
||||
GetBuilder<CameraClassController>(
|
||||
builder: (cameraClassController) => Expanded(
|
||||
child:
|
||||
Text(cameraClassController.scannedText.toString())))
|
||||
],
|
||||
)
|
||||
],
|
||||
isleading: true);
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return MyScafolld(
|
||||
// title: 'Scan Id'.tr,
|
||||
// body: [
|
||||
// Column(
|
||||
// children: [
|
||||
// Padding(
|
||||
// padding:
|
||||
// const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
// child: GetBuilder<CameraClassController>(
|
||||
// builder: (cameraClassController) =>
|
||||
// cameraClassController.isCameraInitialized
|
||||
// ? Stack(
|
||||
// children: [
|
||||
// Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// child: FittedBox(
|
||||
// fit: BoxFit.fitWidth,
|
||||
// child: SizedBox(
|
||||
// width: Get.width * .9,
|
||||
// height: Get.width *
|
||||
// .9, // Set the desired aspect ratio here
|
||||
// child: CameraPreview(
|
||||
// cameraClassController.cameraController,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 72,
|
||||
// child: Container(
|
||||
// width: 230,
|
||||
// height: 25,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.yellowColor,
|
||||
// width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 60,
|
||||
// right: 5,
|
||||
// child: Container(
|
||||
// width: 230,
|
||||
// height: 25,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 156,
|
||||
// right: 5,
|
||||
// child: Container(
|
||||
// width: 140,
|
||||
// height: 20,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 175,
|
||||
// right: 5,
|
||||
// child: Container(
|
||||
// width: 140,
|
||||
// height: 15,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 191,
|
||||
// right: 5,
|
||||
// child: Container(
|
||||
// width: 140,
|
||||
// height: 15,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 207,
|
||||
// right: 5,
|
||||
// child: Container(
|
||||
// width: 140,
|
||||
// height: 15,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 225,
|
||||
// right: 5,
|
||||
// child: Container(
|
||||
// width: 140,
|
||||
// height: 15,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 115,
|
||||
// left: 25,
|
||||
// child: Container(
|
||||
// width: 120,
|
||||
// height: 110,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// )
|
||||
// : Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// height: Get.width * 3 / 4,
|
||||
// width: Get.width,
|
||||
// child: Center(
|
||||
// child: Text(
|
||||
// 'Camera not initilaized yet'.tr,
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
// children: [
|
||||
// MyElevatedButton(
|
||||
// title: 'Scan ID MklGoogle'.tr, onPressed: () {}),
|
||||
// // cameraClassController.takePictureAndMLGoogleScan()),
|
||||
// MyElevatedButton(
|
||||
// title: 'Scan ID Tesseract'.tr, onPressed: () {}),
|
||||
// ],
|
||||
// ),
|
||||
// MyElevatedButton(
|
||||
// title: 'Scan ID Api'.tr,
|
||||
// onPressed: () => cameraClassController.extractCardId()),
|
||||
// GetBuilder<CameraClassController>(
|
||||
// builder: (cameraClassController) => Expanded(
|
||||
// child:
|
||||
// Text(cameraClassController.scannedText.toString())))
|
||||
// ],
|
||||
// )
|
||||
// ],
|
||||
// isleading: true);
|
||||
// }
|
||||
// }
|
||||
|
||||
class CameraWidgetPassPort extends StatelessWidget {
|
||||
final CameraClassController cameraClassController =
|
||||
Get.put(CameraClassController());
|
||||
// class CameraWidgetPassPort extends StatelessWidget {
|
||||
// final CameraClassController cameraClassController =
|
||||
// Get.put(CameraClassController());
|
||||
|
||||
CameraWidgetPassPort({super.key});
|
||||
// CameraWidgetPassPort({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'Scan Id'.tr,
|
||||
body: [
|
||||
Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: GetBuilder<CameraClassController>(
|
||||
builder: (cameraClassController) =>
|
||||
cameraClassController.isCameraInitialized
|
||||
? Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fitWidth,
|
||||
child: SizedBox(
|
||||
width: Get.width * .9,
|
||||
height: Get.width *
|
||||
.9, // Set the desired aspect ratio here
|
||||
child: CameraPreview(
|
||||
cameraClassController.cameraController,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 35,
|
||||
left: 35,
|
||||
width: Get.width * .77,
|
||||
height:
|
||||
17, //left":95.0,"top":134.0,"width":2909.0,"height":175.0
|
||||
child: Container(
|
||||
// width: 230,
|
||||
// height: 25,
|
||||
decoration: BoxDecoration(
|
||||
// color: AppColor.blueColor,
|
||||
border: Border.all(
|
||||
color: AppColor.yellowColor,
|
||||
width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 60,
|
||||
right: 25,
|
||||
width: 90,
|
||||
height: 25,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
// color: AppColor.blueColor,
|
||||
border: Border.all(
|
||||
color: AppColor.blueColor, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 110,
|
||||
right: 90,
|
||||
child: Container(
|
||||
width: 140,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
// color: AppColor.blueColor,
|
||||
border: Border.all(
|
||||
color: AppColor.blueColor, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
height: Get.width * 3 / 4,
|
||||
width: Get.width,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Camera not initilaized yet',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
title: 'Scan ID MklGoogle'.tr, onPressed: () => {}),
|
||||
// cameraClassController.takePictureAndMLGoogleScan()),
|
||||
MyElevatedButton(
|
||||
title: 'Scan ID Tesseract'.tr, onPressed: () {}),
|
||||
],
|
||||
),
|
||||
MyElevatedButton(
|
||||
title: 'Scan ID Api'.tr,
|
||||
onPressed: () => cameraClassController.extractCardId()),
|
||||
GetBuilder<CameraClassController>(
|
||||
builder: (cameraClassController) => Expanded(
|
||||
child:
|
||||
Text(cameraClassController.scannedText.toString())))
|
||||
],
|
||||
)
|
||||
],
|
||||
isleading: true);
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return MyScafolld(
|
||||
// title: 'Scan Id'.tr,
|
||||
// body: [
|
||||
// Column(
|
||||
// children: [
|
||||
// Padding(
|
||||
// padding:
|
||||
// const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
// child: GetBuilder<CameraClassController>(
|
||||
// builder: (cameraClassController) =>
|
||||
// cameraClassController.isCameraInitialized
|
||||
// ? Stack(
|
||||
// children: [
|
||||
// Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// child: FittedBox(
|
||||
// fit: BoxFit.fitWidth,
|
||||
// child: SizedBox(
|
||||
// width: Get.width * .9,
|
||||
// height: Get.width *
|
||||
// .9, // Set the desired aspect ratio here
|
||||
// child: CameraPreview(
|
||||
// cameraClassController.cameraController,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 35,
|
||||
// left: 35,
|
||||
// width: Get.width * .77,
|
||||
// height:
|
||||
// 17, //left":95.0,"top":134.0,"width":2909.0,"height":175.0
|
||||
// child: Container(
|
||||
// // width: 230,
|
||||
// // height: 25,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.yellowColor,
|
||||
// width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 60,
|
||||
// right: 25,
|
||||
// width: 90,
|
||||
// height: 25,
|
||||
// child: Container(
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 110,
|
||||
// right: 90,
|
||||
// child: Container(
|
||||
// width: 140,
|
||||
// height: 20,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// )
|
||||
// : Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// height: Get.width * 3 / 4,
|
||||
// width: Get.width,
|
||||
// child: Center(
|
||||
// child: Text(
|
||||
// 'Camera not initilaized yet',
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
// children: [
|
||||
// MyElevatedButton(
|
||||
// title: 'Scan ID MklGoogle'.tr, onPressed: () => {}),
|
||||
// // cameraClassController.takePictureAndMLGoogleScan()),
|
||||
// MyElevatedButton(
|
||||
// title: 'Scan ID Tesseract'.tr, onPressed: () {}),
|
||||
// ],
|
||||
// ),
|
||||
// MyElevatedButton(
|
||||
// title: 'Scan ID Api'.tr,
|
||||
// onPressed: () => cameraClassController.extractCardId()),
|
||||
// GetBuilder<CameraClassController>(
|
||||
// builder: (cameraClassController) => Expanded(
|
||||
// child:
|
||||
// Text(cameraClassController.scannedText.toString())))
|
||||
// ],
|
||||
// )
|
||||
// ],
|
||||
// isleading: true);
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -152,9 +152,11 @@ class InstructionsOfRoads extends StatelessWidget {
|
||||
if (controller.currentInstruction.isEmpty) return const SizedBox();
|
||||
|
||||
return TweenAnimationBuilder<double>(
|
||||
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
builder: (context, value, child) {
|
||||
final theme = Theme.of(context);
|
||||
return Transform.translate(
|
||||
offset: Offset(0, 50 * (1 - value)), // حركة انزلاق
|
||||
child: Opacity(
|
||||
@@ -163,16 +165,15 @@ class InstructionsOfRoads extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1F1F1F)
|
||||
.withOpacity(0.95), // خلفية داكنة
|
||||
color: theme.cardColor.withOpacity(0.95), // Adaptive background
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5)),
|
||||
],
|
||||
border: Border.all(color: Colors.white.withOpacity(0.1)),
|
||||
border: Border.all(color: theme.dividerColor.withOpacity(0.1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -187,7 +188,6 @@ class InstructionsOfRoads extends StatelessWidget {
|
||||
color: Colors.white, size: 24),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
|
||||
// نص التعليمات
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -195,18 +195,15 @@ class InstructionsOfRoads extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
"NEXT STEP".tr,
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade500,
|
||||
fontSize: 10,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.hintColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
controller.currentInstruction,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2),
|
||||
maxLines: 2,
|
||||
@@ -216,6 +213,7 @@ class InstructionsOfRoads extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// فاصل عمودي
|
||||
Container(
|
||||
width: 1,
|
||||
@@ -278,12 +276,13 @@ class CancelWidget extends StatelessWidget {
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
color: Theme.of(context).cardColor.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 8)
|
||||
BoxShadow(color: Theme.of(context).shadowColor.withOpacity(0.1), blurRadius: 8)
|
||||
],
|
||||
),
|
||||
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
@@ -379,16 +378,17 @@ class PricesWindow extends StatelessWidget {
|
||||
width: Get.width * 0.85,
|
||||
padding: const EdgeInsets.all(30),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
color: Theme.of(context).shadowColor.withOpacity(0.2),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -411,11 +411,12 @@ class PricesWindow extends StatelessWidget {
|
||||
Text(
|
||||
'${controller.totalCost} ${'\$'.tr}',
|
||||
style: AppStyle.headTitle2.copyWith(
|
||||
color: Colors.black87,
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
fontSize: 42,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
import '../../../../controller/auth/captin/history_captain.dart';
|
||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||
|
||||
@@ -13,74 +14,159 @@ class HistoryCaptain extends StatelessWidget {
|
||||
Get.put(HistoryCaptainController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100], // A softer background color
|
||||
appBar: AppBar(
|
||||
title: Text('Ride History'.tr),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 1,
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
body: GetBuilder<HistoryCaptainController>(
|
||||
builder: (controller) {
|
||||
if (controller.isloading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.historyData['message'].isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.history_toggle_off,
|
||||
size: 80, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No Rides Yet'.tr,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: FinanceDesignSystem.primaryDark),
|
||||
);
|
||||
}
|
||||
|
||||
// 动画: Wrap ListView with AnimationLimiter for staggered animations
|
||||
return AnimationLimiter(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
itemCount: controller.historyData['message'].length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var trip = controller.historyData['message'][index];
|
||||
final List trips = controller.historyData['message'] ?? [];
|
||||
|
||||
// 动画: Apply animation to each list item
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: _AnimatedHistoryCard(
|
||||
trip: trip,
|
||||
onTap: () {
|
||||
// Your original logic is preserved here
|
||||
if (trip['status'] != 'Cancel') {
|
||||
controller.getHistoryDetails(trip['order_id']);
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'This Trip Was Cancelled'.tr,
|
||||
'This Trip Was Cancelled'.tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
return CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
// 1. Custom App Bar
|
||||
SliverAppBar(
|
||||
expandedHeight: 180,
|
||||
pinned: true,
|
||||
stretch: true,
|
||||
backgroundColor: FinanceDesignSystem.primaryDark,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Ride History'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
background: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: FinanceDesignSystem.balanceGradient,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: -50,
|
||||
top: -20,
|
||||
child: Icon(
|
||||
Icons.history_rounded,
|
||||
size: 200,
|
||||
color: Colors.white.withOpacity(0.05),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
trips.length.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Total Rides'.tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 2. Trips List
|
||||
if (trips.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.history_toggle_off_rounded,
|
||||
size: 64,
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'No Rides Yet'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Your completed trips will appear here'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 40),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final trip = trips[index];
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: _TripHistoryCard(
|
||||
trip: trip,
|
||||
onTap: () {
|
||||
if (trip['status'] != 'Cancel' &&
|
||||
trip['status'] != 'CancelByPassenger') {
|
||||
controller
|
||||
.getHistoryDetails(trip['order_id']);
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'This Trip Was Cancelled'.tr,
|
||||
'This Trip Was Cancelled'.tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: trips.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -88,97 +174,159 @@ class HistoryCaptain extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// 动画: A new stateful widget to handle the tap animation
|
||||
class _AnimatedHistoryCard extends StatefulWidget {
|
||||
final Map<String, dynamic> trip;
|
||||
class _TripHistoryCard extends StatelessWidget {
|
||||
final Map trip;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _AnimatedHistoryCard({required this.trip, required this.onTap});
|
||||
const _TripHistoryCard({required this.trip, required this.onTap});
|
||||
|
||||
@override
|
||||
__AnimatedHistoryCardState createState() => __AnimatedHistoryCardState();
|
||||
}
|
||||
|
||||
class __AnimatedHistoryCardState extends State<_AnimatedHistoryCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scaleAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
);
|
||||
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTapDown(TapDownDetails details) {
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
_controller.reverse();
|
||||
widget.onTap();
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
_controller.reverse();
|
||||
String _formatDateToSyria(String? dateStr) {
|
||||
if (dateStr == null) return '';
|
||||
try {
|
||||
// Parse GMT date
|
||||
DateTime date = DateTime.parse(dateStr);
|
||||
// Add 3 hours for Syria (GMT+3)
|
||||
DateTime syriaDate = date.add(const Duration(hours: 3));
|
||||
// Format to readable string
|
||||
return DateFormat('yyyy-MM-dd HH:mm').format(syriaDate);
|
||||
} catch (e) {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTapDown: _onTapDown,
|
||||
onTapUp: _onTapUp,
|
||||
onTapCancel: _onTapCancel,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.receipt_long,
|
||||
color: Theme.of(context).primaryColor, size: 40),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${'OrderId'.tr}: ${widget.trip['order_id']}',
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.trip['created_at'],
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.primaryDark
|
||||
.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.receipt_long_rounded,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${'OrderId'.tr} #${trip['order_id']}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
_formatDateToSyria(trip['created_at']),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildStatusChip(trip['status']),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
const Icon(Icons.circle,
|
||||
size: 12, color: FinanceDesignSystem.accentBlue),
|
||||
Container(
|
||||
width: 2,
|
||||
height: 20,
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
const Icon(Icons.location_on_rounded,
|
||||
size: 14, color: FinanceDesignSystem.dangerRed),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
trip['start_name'] ?? 'Pickup Location'.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
trip['end_name'] ?? 'Destination Location'.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trip['price'] != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${trip['price']} ${'SYP'.tr}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 16,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_forward_ios_rounded,
|
||||
size: 12, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildStatusChip(widget.trip['status']),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -186,46 +334,78 @@ class __AnimatedHistoryCardState extends State<_AnimatedHistoryCard>
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🎨 A separate function for the status chip, slightly restyled for Material
|
||||
Widget _buildStatusChip(String status) {
|
||||
Color chipColor;
|
||||
Color textColor;
|
||||
String statusText = status;
|
||||
IconData iconData;
|
||||
Widget _buildStatusChip(String? status) {
|
||||
Color color;
|
||||
IconData icon;
|
||||
String label = status ?? 'Unknown';
|
||||
List<Color> gradientColors;
|
||||
|
||||
switch (status) {
|
||||
case 'Apply':
|
||||
chipColor = Colors.green.shade50;
|
||||
textColor = Colors.green.shade800;
|
||||
iconData = Icons.check_circle;
|
||||
break;
|
||||
case 'Refused':
|
||||
chipColor = Colors.red.shade50;
|
||||
textColor = Colors.red.shade800;
|
||||
iconData = Icons.cancel;
|
||||
break;
|
||||
case 'Cancel':
|
||||
chipColor = Colors.orange.shade50;
|
||||
textColor = Colors.orange.shade800;
|
||||
iconData = Icons.info;
|
||||
statusText = 'Cancelled';
|
||||
break;
|
||||
default:
|
||||
chipColor = Colors.grey.shade200;
|
||||
textColor = Colors.grey.shade800;
|
||||
iconData = Icons.hourglass_empty;
|
||||
switch (status) {
|
||||
case 'Apply':
|
||||
color = FinanceDesignSystem.successGreen;
|
||||
icon = Icons.check_circle_rounded;
|
||||
label = 'Completed'.tr;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
break;
|
||||
case 'Refused':
|
||||
color = FinanceDesignSystem.dangerRed;
|
||||
icon = Icons.cancel_rounded;
|
||||
label = 'Refused'.tr;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
break;
|
||||
case 'Cancel':
|
||||
color = Colors.orange;
|
||||
icon = Icons.info_rounded;
|
||||
label = 'Cancelled'.tr;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
break;
|
||||
case 'CancelByPassenger':
|
||||
color = const Color(0xFF6B4EFF);
|
||||
icon = Icons.person_off_rounded;
|
||||
label = 'Cancelled by Passenger'.tr;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
break;
|
||||
default:
|
||||
color = Colors.grey;
|
||||
icon = Icons.help_rounded;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: gradientColors,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withOpacity(0.3), width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: color),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Chip(
|
||||
avatar: Icon(iconData, color: textColor, size: 16),
|
||||
label: Text(
|
||||
statusText.tr,
|
||||
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: chipColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
side: BorderSide.none,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:sefer_driver/constant/api_key.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
|
||||
import 'package:sefer_driver/controller/auth/captin/history_captain.dart';
|
||||
@@ -168,7 +169,7 @@ class _HistoryDetailsPageState extends State<HistoryDetailsPage> {
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
shadowColor: Colors.black.withValues(alpha: 0.1),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
clipBehavior:
|
||||
Clip.antiAlias, // Ensures the map respects the border radius
|
||||
@@ -176,7 +177,8 @@ class _HistoryDetailsPageState extends State<HistoryDetailsPage> {
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 250,
|
||||
child: GoogleMap(
|
||||
child: IntaleqMap(
|
||||
apiKey: AK.mapAPIKEY,
|
||||
initialCameraPosition: initialCameraPosition,
|
||||
markers: markers,
|
||||
polylines: {
|
||||
@@ -188,7 +190,7 @@ class _HistoryDetailsPageState extends State<HistoryDetailsPage> {
|
||||
width: 5,
|
||||
),
|
||||
},
|
||||
onMapCreated: (GoogleMapController mapController) {
|
||||
onMapCreated: (IntaleqMapController mapController) {
|
||||
// Animate camera to fit the route
|
||||
if (startLocation != null && endLocation != null) {
|
||||
LatLngBounds bounds = LatLngBounds(
|
||||
@@ -210,7 +212,7 @@ class _HistoryDetailsPageState extends State<HistoryDetailsPage> {
|
||||
),
|
||||
);
|
||||
mapController.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(bounds, 60.0));
|
||||
CameraUpdate.newLatLngBounds(bounds, left: 60, top: 60, right: 60, bottom: 60));
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -252,7 +254,7 @@ class _DetailCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withOpacity(0.05),
|
||||
shadowColor: Colors.black.withValues(alpha: 0.05),
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
|
||||
@@ -5,7 +5,9 @@ import 'package:sefer_driver/views/home/Captin/home_captain/help_details_replay_
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../widgets/my_scafold.dart';
|
||||
|
||||
class HelpCaptain extends StatelessWidget {
|
||||
HelpCaptain({super.key});
|
||||
@@ -13,162 +15,158 @@ class HelpCaptain extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(HelpController());
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(
|
||||
'Helping Page'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Icon(CupertinoIcons.back),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(
|
||||
20.0), // Increased padding around the content
|
||||
child: ListView(
|
||||
// crossAxisAlignment:
|
||||
// CrossAxisAlignment.stretch, // Stretch children to full width
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return MyScafolld(
|
||||
title: 'Helping Page'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.all(18.0), // Slightly increased padding
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
CupertinoTheme.of(context).brightness == Brightness.light
|
||||
? CupertinoColors.systemGrey6
|
||||
: CupertinoColors.darkBackgroundGray,
|
||||
borderRadius:
|
||||
BorderRadius.circular(15.0), // More rounded corners
|
||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
border: Border.all(color: theme.dividerColor),
|
||||
),
|
||||
child: Text(
|
||||
'If you need any help or have questions, this is the right place to do that. You are welcome!'
|
||||
.tr,
|
||||
style:
|
||||
CupertinoTheme.of(context).textTheme.textStyle.copyWith(
|
||||
fontSize: 16, // Slightly larger font size
|
||||
color: CupertinoColors.label.resolveFrom(
|
||||
context), // Ensure text color adapts to theme
|
||||
),
|
||||
style: theme.textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
CupertinoFormSection.insetGrouped(
|
||||
// Using CupertinoFormSection for better styling
|
||||
header: Text('Submit Your Question'.tr),
|
||||
margin: EdgeInsets.zero,
|
||||
children: [
|
||||
GetBuilder<HelpController>(
|
||||
builder: (helpController) => Form(
|
||||
key: helpController.formKey,
|
||||
child: CupertinoTextFormFieldRow(
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Submit Your Question'.tr,
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GetBuilder<HelpController>(
|
||||
builder: (helpController) => Form(
|
||||
key: helpController.formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: helpController.helpQuestionController,
|
||||
placeholder: 'Enter your Question here'.tr,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter your Question here'.tr,
|
||||
prefixIcon:
|
||||
const Icon(Icons.question_answer_outlined),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
maxLines: 3,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your question'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
prefix: const Icon(CupertinoIcons
|
||||
.question_circle), // Added a prefix icon
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0, vertical: 10.0),
|
||||
child: GetBuilder<HelpController>(
|
||||
builder: (helpController) => helpController.isLoading
|
||||
? const CupertinoActivityIndicator()
|
||||
: CupertinoButton.filled(
|
||||
onPressed: () {
|
||||
if (helpController.formKey.currentState!
|
||||
.validate()) {
|
||||
helpController.addHelpQuestion();
|
||||
helpController.helpQuestionController
|
||||
.clear(); // Clear the text field
|
||||
}
|
||||
},
|
||||
child: Text('Submit Question'.tr),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Your Questions'.tr,
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.navTitleTextStyle
|
||||
.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: GetBuilder<HelpController>(
|
||||
builder: (helpController) =>
|
||||
CupertinoListSection.insetGrouped(
|
||||
margin: EdgeInsets.zero,
|
||||
children: helpController.helpQuestionDate['message'] != null
|
||||
? List.generate(
|
||||
helpController.helpQuestionDate['message'].length,
|
||||
(index) {
|
||||
var list = helpController
|
||||
.helpQuestionDate['message'][index];
|
||||
return CupertinoListTile(
|
||||
title: Text(
|
||||
EncryptionHelper.instance
|
||||
.decryptData(list['helpQuestion']),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
list['datecreated'],
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.tabLabelTextStyle,
|
||||
),
|
||||
const Icon(CupertinoIcons.chevron_forward),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
helpController.getIndex(
|
||||
int.parse(EncryptionHelper.instance
|
||||
.decryptData(list['id'])),
|
||||
EncryptionHelper.instance
|
||||
.decryptData(list['helpQuestion']));
|
||||
helpController
|
||||
.getHelpRepley(list['id'].toString());
|
||||
Get.to(() => const HelpDetailsReplayPage());
|
||||
const SizedBox(height: 16),
|
||||
helpController.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (helpController.formKey.currentState!
|
||||
.validate()) {
|
||||
helpController.addHelpQuestion();
|
||||
helpController.helpQuestionController
|
||||
.clear();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
: [
|
||||
Center(
|
||||
child: Text('No questions asked yet.'.tr),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text('Submit Question'.tr),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Your Questions'.tr,
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GetBuilder<HelpController>(
|
||||
builder: (helpController) {
|
||||
final questions = helpController.helpQuestionDate['message'];
|
||||
if (questions == null || questions.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Text('No questions asked yet.'.tr),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: questions.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
var list = questions[index];
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: theme.dividerColor),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
EncryptionHelper.instance
|
||||
.decryptData(list['helpQuestion']),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
list['datecreated'],
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
trailing:
|
||||
const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () {
|
||||
helpController.getIndex(
|
||||
int.parse(EncryptionHelper.instance
|
||||
.decryptData(list['id'])),
|
||||
EncryptionHelper.instance
|
||||
.decryptData(list['helpQuestion']));
|
||||
helpController.getHelpRepley(list['id'].toString());
|
||||
Get.to(() => const HelpDetailsReplayPage());
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// class HelpCaptain extends StatelessWidget {
|
||||
// HelpCaptain({super.key});
|
||||
|
||||
|
||||
@@ -19,65 +19,90 @@ class HelpDetailsReplayPage extends StatelessWidget {
|
||||
body: [
|
||||
helpController.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Card(
|
||||
elevation: 3,
|
||||
child: Container(
|
||||
width: Get.width * .66,
|
||||
color: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
helpController.qustion,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Card(
|
||||
elevation: 3,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
width: Get.width * .66,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: helpController.status ==
|
||||
'not yet' ||
|
||||
EncryptionHelper.instance
|
||||
.decryptData(helpController
|
||||
.helpQuestionRepleyDate[
|
||||
'message']['replay'])
|
||||
.toString() ==
|
||||
'not yet'
|
||||
? Text(
|
||||
'No Response yet.'.tr,
|
||||
style: AppStyle.title,
|
||||
)
|
||||
: Text(
|
||||
EncryptionHelper.instance
|
||||
.decryptData(helpController
|
||||
.helpQuestionRepleyDate[
|
||||
'message']['replay'])
|
||||
.toString(),
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// Question Bubble (Aligned to Start/End based on locale, usually start for sender)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: Get.width * 0.75),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Your Question'.tr,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
helpController.qustion,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Reply Bubble (Aligned to opposite side)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: Get.width * 0.75),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Support Reply'.tr,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Builder(builder: (context) {
|
||||
final replayData = helpController.helpQuestionRepleyDate['message'];
|
||||
final isNoReply = helpController.status == 'not yet' ||
|
||||
replayData == null ||
|
||||
EncryptionHelper.instance.decryptData(replayData['replay']).toString() == 'not yet';
|
||||
|
||||
return Text(
|
||||
isNoReply
|
||||
? 'No Response yet.'.tr
|
||||
: EncryptionHelper.instance.decryptData(replayData['replay']).toString(),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
],
|
||||
isleading: true,
|
||||
));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,83 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../../constant/api_key.dart';
|
||||
import '../../../../controller/functions/location_controller.dart';
|
||||
import '../../../../controller/home/captin/home_captain_controller.dart';
|
||||
// هذه ويدجت بديلة للـ _MapView في الكود الخاص بك
|
||||
// V3 - MapView Replacement using flutter_map
|
||||
|
||||
class OsmMapView extends StatelessWidget {
|
||||
const OsmMapView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// افترض أنك تحصل على الموقع والاتجاه بنفس الطريقة من الكونترولر
|
||||
final LocationController locationController =
|
||||
Get.find<LocationController>();
|
||||
final HomeCaptainController homeCaptainController =
|
||||
Get.find<HomeCaptainController>();
|
||||
|
||||
// يمكنك استخدام GetBuilder لمراقبة التغييرات في الموقع
|
||||
return Obx(() {
|
||||
final LatLng currentLocation = LatLng(
|
||||
locationController.myLocation.latitude,
|
||||
locationController.myLocation.longitude);
|
||||
final double currentHeading = locationController.heading;
|
||||
|
||||
return FlutterMap(
|
||||
// يمكنك ربط هذا بـ MapController الخاص بـ flutter_map
|
||||
// mapController: homeCaptainController.flutterMapController,
|
||||
options: MapOptions(
|
||||
initialCenter: currentLocation,
|
||||
initialZoom: 15,
|
||||
maxZoom: 18,
|
||||
minZoom: 6,
|
||||
// تدوير الخريطة (اختياري)
|
||||
initialRotation: currentHeading,
|
||||
return IntaleqMap(
|
||||
apiKey: AK.mapSaasKey,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: currentLocation,
|
||||
zoom: 15,
|
||||
bearing: currentHeading,
|
||||
),
|
||||
children: [
|
||||
// 1. طبقة الخريطة الأساسية (Tiles)
|
||||
// هذا هو الرابط لخرائط OSM الأساسية
|
||||
TileLayer(
|
||||
urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
subdomains: ['a', 'b', 'c'],
|
||||
userAgentPackageName:
|
||||
'com.example.app', // استبدل باسم الباكج الخاص بك
|
||||
|
||||
// لاستخدام الخرائط الأوفلاين (بعد إعداد flutter_map_tile_caching)
|
||||
// tileProvider: CachedTileProvider(),
|
||||
),
|
||||
|
||||
// 2. طبقة العلامات (Markers)
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
Marker(
|
||||
width: 80.0,
|
||||
height: 80.0,
|
||||
point: currentLocation,
|
||||
child: Transform.rotate(
|
||||
angle: currentHeading *
|
||||
(3.1415926535 / 180), // تحويل من درجات إلى راديان
|
||||
child: Image.asset(
|
||||
'assets/images/car_icon.png', // تأكد أن لديك أيقونة السيارة
|
||||
// يمكنك استخدام نفس الـ carIcon من الكونترولر
|
||||
// icon: homeCaptainController.carIcon, (ملاحظة: flutter_map تستخدم ويدجت)
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// يمكنك إضافة طبقات أخرى هنا (مثل الخطوط Polylines أو المضلعات Polygons)
|
||||
],
|
||||
markers: {
|
||||
Marker(
|
||||
markerId: const MarkerId('myLocation'),
|
||||
position: currentLocation,
|
||||
rotation: currentHeading,
|
||||
icon: homeCaptainController.carIcon,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
flat: true,
|
||||
zIndex: 2,
|
||||
)
|
||||
},
|
||||
onMapCreated: (IntaleqMapController controller) {
|
||||
// You can assign this controller if needed
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ملاحظة: ستحتاج إلى تعديل بسيط في HomeCaptainController
|
||||
// لإنشاء MapController الخاص بـ flutter_map بدلاً من GoogleMapController
|
||||
// import 'package:flutter_map/flutter_map.dart';
|
||||
// MapController flutterMapController = MapController();
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
|
||||
|
||||
import '../../../../../constant/style.dart';
|
||||
import '../../../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../../widgets/elevated_btn.dart';
|
||||
import '../../../../../controller/home/captin/home_captain_controller.dart';
|
||||
|
||||
@@ -91,45 +90,56 @@ class ConnectWidget extends StatelessWidget {
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: homeCaptainController.isActive
|
||||
? [Colors.green.shade400, Colors.green.shade700]
|
||||
: [Colors.grey.shade400, Colors.grey.shade700],
|
||||
? [const Color(0xFF00C853), const Color(0xFF00E676)]
|
||||
: [Colors.grey.shade600, Colors.grey.shade400],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: homeCaptainController.isActive
|
||||
? Colors.green.withOpacity(0.3)
|
||||
: Colors.grey.withOpacity(0.3),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
color: (homeCaptainController.isActive
|
||||
? const Color(0xFF00C853)
|
||||
: Colors.grey)
|
||||
.withOpacity(0.4),
|
||||
spreadRadius: 0,
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CupertinoButton(
|
||||
onPressed: homeCaptainController.onButtonSelected,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 12),
|
||||
horizontal: 20, vertical: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
homeCaptainController.isActive
|
||||
? CupertinoIcons.check_mark_circled_solid
|
||||
: CupertinoIcons.circle,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
homeCaptainController.isActive
|
||||
? Icons.power_settings_new_rounded
|
||||
: Icons.power_off_rounded,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
homeCaptainController.isActive
|
||||
? 'Connected'.tr
|
||||
: 'Not Connected'.tr,
|
||||
? 'Online'.tr
|
||||
: 'Offline'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/controller/firebase/local_notification.dart';
|
||||
import 'package:sefer_driver/main.dart';
|
||||
import 'package:sefer_driver/views/auth/captin/otp_page.dart';
|
||||
import 'package:sefer_driver/views/home/Captin/driver_map_page.dart';
|
||||
import 'package:sefer_driver/views/home/Captin/orderCaptin/vip_order_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -13,7 +12,6 @@ import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||
|
||||
import '../../../../../constant/colors.dart';
|
||||
import '../../../../../constant/links.dart';
|
||||
import '../../../../../controller/firebase/firbase_messge.dart';
|
||||
import '../../../../../controller/firebase/notification_service.dart';
|
||||
import '../../../../../controller/functions/crud.dart';
|
||||
import '../../../../../controller/home/captin/order_request_controller.dart';
|
||||
@@ -21,319 +19,279 @@ import '../../../../../controller/home/navigation/navigation_view.dart';
|
||||
import '../../../../../print.dart';
|
||||
import '../../../../Rate/ride_calculate_driver.dart';
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Design Tokens (Responsive)
|
||||
// ─────────────────────────────────────────────
|
||||
class _T {
|
||||
static Color surface(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF16213E)
|
||||
: Colors.white;
|
||||
|
||||
static const Color accent = Color(0xFFF0A500);
|
||||
static const Color blue = Color(0xFF3498DB);
|
||||
|
||||
static Color border(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF2A2A4A)
|
||||
: Colors.grey.withOpacity(0.2);
|
||||
|
||||
static const double radius = 14.0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Left Side Menu
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// Returns the vertical icon column anchored to the left-center of the map.
|
||||
GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
|
||||
final firebaseMessagesController =
|
||||
Get.isRegistered<FirebaseMessagesController>()
|
||||
? Get.find<FirebaseMessagesController>()
|
||||
: Get.put(FirebaseMessagesController());
|
||||
return GetBuilder<HomeCaptainController>(
|
||||
builder: (controller) => Positioned(
|
||||
bottom: Get.height * .2,
|
||||
left: 6,
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(microseconds: 200),
|
||||
width: controller.widthMapTypeAndTraffic,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
border: Border.all(color: AppColor.blueColor),
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: Builder(builder: (context) {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
await checkForPendingOrderFromServer();
|
||||
box.read(BoxName.rideArgumentsFromBackground) != 'failure'
|
||||
? Get.to(() => PassengerLocationMapPage(),
|
||||
arguments:
|
||||
box.read(BoxName.rideArgumentsFromBackground))
|
||||
: MyDialog().getDialog(
|
||||
'Ride info'.tr,
|
||||
'you dont have accepted ride'.tr,
|
||||
() {
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
// 'box.read(BoxName.rideArgumentsFromBackground): ${box.read(BoxName.rideArgumentsFromBackground)}');
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.directions_car_rounded,
|
||||
size: 29,
|
||||
color:
|
||||
box.read(BoxName.rideArgumentsFromBackground) == 'failure'
|
||||
? AppColor.redColor
|
||||
: AppColor.greenColor,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(microseconds: 200),
|
||||
width: controller.widthMapTypeAndTraffic,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
border: Border.all(color: AppColor.blueColor),
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: IconButton(
|
||||
onLongPress: () {
|
||||
box.write(BoxName.statusDriverLocation, 'off');
|
||||
},
|
||||
onPressed: () {
|
||||
// NotificationController1()
|
||||
// .showNotification('Sefer Driver'.tr, ''.tr, '', '');
|
||||
final now = DateTime.now();
|
||||
DateTime? lastRequestTime =
|
||||
box.read(BoxName.lastTimeStaticThrottle);
|
||||
|
||||
if (lastRequestTime == null ||
|
||||
now.difference(lastRequestTime).inMinutes >= 2) {
|
||||
// Update the last request time to now
|
||||
lastRequestTime = now;
|
||||
box.write(BoxName.lastTimeStaticThrottle, lastRequestTime);
|
||||
// Navigate to the RideCalculateDriver page
|
||||
Get.to(() => RideCalculateDriver());
|
||||
} else {
|
||||
// Optionally show a message or handle the throttling case
|
||||
final minutesLeft =
|
||||
2 - now.difference(lastRequestTime).inMinutes;
|
||||
// Get.snackbar(
|
||||
// '${'Please wait'.tr} $minutesLeft ${"minutes before trying again.".tr}',
|
||||
// '');
|
||||
NotificationController().showNotification(
|
||||
'Intaleq Driver'.tr,
|
||||
'${'Please wait'.tr} $minutesLeft ${"minutes before trying again.".tr}',
|
||||
'ding',
|
||||
'');
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
FontAwesome5.chart_bar,
|
||||
size: 29,
|
||||
color: AppColor.blueColor,
|
||||
builder: (ctrl) => Positioned(
|
||||
// Place just above the bottom status bar
|
||||
bottom: 100,
|
||||
left: 10,
|
||||
child: Builder(builder: (context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _T.surface(context),
|
||||
borderRadius: BorderRadius.circular(_T.radius),
|
||||
border: Border.all(color: _T.border(context)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(2, 4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
// Platform.isAndroid
|
||||
// ?
|
||||
int.parse(box.read(BoxName.carYear).toString()) > 2023
|
||||
? AnimatedContainer(
|
||||
duration: const Duration(microseconds: 200),
|
||||
width: controller.widthMapTypeAndTraffic,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
border: Border.all(color: AppColor.blueColor),
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: Builder(builder: (context) {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
// mySnakeBarError('ad');
|
||||
Get.to(() => const VipOrderPage());
|
||||
},
|
||||
icon: const Icon(
|
||||
Octicons.watch,
|
||||
size: 29,
|
||||
color: AppColor.blueColor,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// ── 1. Active Ride shortcut ──────────
|
||||
_MenuIcon(
|
||||
icon: Icons.directions_car_rounded,
|
||||
color:
|
||||
box.read(BoxName.rideArgumentsFromBackground) == 'failure'
|
||||
? Colors.red.shade400
|
||||
: Colors.green.shade400,
|
||||
tooltip: 'Active Ride'.tr,
|
||||
onTap: () async {
|
||||
await checkForPendingOrderFromServer();
|
||||
if (box.read(BoxName.rideArgumentsFromBackground) != 'failure') {
|
||||
Get.to(
|
||||
() => PassengerLocationMapPage(),
|
||||
arguments: box.read(BoxName.rideArgumentsFromBackground),
|
||||
);
|
||||
}),
|
||||
)
|
||||
: const SizedBox(),
|
||||
// const SizedBox(
|
||||
// height: 5,
|
||||
// ),
|
||||
|
||||
AnimatedContainer(
|
||||
duration: const Duration(microseconds: 200),
|
||||
width: controller.widthMapTypeAndTraffic,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
border: Border.all(color: AppColor.blueColor),
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: Builder(builder: (context) {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
box.remove(BoxName.agreeTerms);
|
||||
Get.to(() => const NavigationView());
|
||||
|
||||
// box.write(BoxName.statusDriverLocation, 'off');
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'Ride info'.tr,
|
||||
'you dont have accepted ride'.tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
FontAwesome5.map,
|
||||
size: 29,
|
||||
color: AppColor.blueColor,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
// AnimatedContainer(
|
||||
// duration: const Duration(microseconds: 200),
|
||||
// width: controller.widthMapTypeAndTraffic,
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColor.secondaryColor,
|
||||
// border: Border.all(color: AppColor.blueColor),
|
||||
// borderRadius: BorderRadius.circular(15)),
|
||||
// child: Builder(builder: (context) {
|
||||
// return IconButton(
|
||||
// onPressed: () async {
|
||||
// NotificationService.sendNotification(
|
||||
// target: 'service', // الإرسال لجميع المشتركين في "service"
|
||||
// title: 'طلب خدمة جديد',
|
||||
// body: 'تم استلام طلب خدمة جديد. الرجاء مراجعة التفاصيل.',
|
||||
// isTopic: true,
|
||||
// category: 'new_service_request', // فئة توضح نوع الإشعار
|
||||
// );
|
||||
// },
|
||||
// icon: const Icon(
|
||||
// FontAwesome5.grin_tears,
|
||||
// size: 29,
|
||||
// color: AppColor.blueColor,
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// ),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
_Divider(context),
|
||||
|
||||
// ── 2. Earnings Chart ────────────────
|
||||
_MenuIcon(
|
||||
icon: FontAwesome5.chart_bar,
|
||||
color: _T.blue,
|
||||
tooltip: 'Earnings'.tr,
|
||||
onTap: () {
|
||||
final now = DateTime.now();
|
||||
DateTime? lastTime = box.read(BoxName.lastTimeStaticThrottle);
|
||||
|
||||
if (lastTime == null ||
|
||||
now.difference(lastTime).inMinutes >= 2) {
|
||||
box.write(BoxName.lastTimeStaticThrottle, now);
|
||||
Get.to(() => RideCalculateDriver());
|
||||
} else {
|
||||
final left = 2 - now.difference(lastTime).inMinutes;
|
||||
NotificationController().showNotification(
|
||||
'Intaleq Driver'.tr,
|
||||
'${'Please wait'.tr} $left ${"minutes before trying again.".tr}',
|
||||
'ding',
|
||||
'',
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () =>
|
||||
box.write(BoxName.statusDriverLocation, 'off'),
|
||||
),
|
||||
|
||||
// ── 3. VIP Orders (2023+ cars only) ──
|
||||
if (int.tryParse(box.read(BoxName.carYear).toString()) != null &&
|
||||
int.parse(box.read(BoxName.carYear).toString()) > 2023) ...[
|
||||
_Divider(context),
|
||||
_MenuIcon(
|
||||
icon: Octicons.watch,
|
||||
color: _T.accent,
|
||||
tooltip: 'VIP Orders'.tr,
|
||||
onTap: () => Get.to(() => const VipOrderPage()),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Reusable sub-widgets
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
class _MenuIcon extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String tooltip;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
const _MenuIcon({
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.tooltip,
|
||||
required this.onTap,
|
||||
this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Tooltip(
|
||||
message: tooltip,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
child: Icon(icon, color: color, size: 26),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Thin separator between icons
|
||||
class _Divider extends StatelessWidget {
|
||||
final BuildContext context;
|
||||
const _Divider(this.context);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
height: 1,
|
||||
width: 32,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||
color: _T.border(this.context),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Server Helpers (unchanged logic)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
Future<void> checkForPendingOrderFromServer() async {
|
||||
bool _isProcessingOrder = false;
|
||||
if (_isProcessingOrder) return;
|
||||
bool isProcessing = false;
|
||||
if (isProcessing) return;
|
||||
|
||||
final driverId = box.read(BoxName.driverID)?.toString();
|
||||
if (driverId == null) return; // Can't check without a driver ID
|
||||
if (driverId == null) return;
|
||||
|
||||
_isProcessingOrder = true; // Lock
|
||||
isProcessing = true;
|
||||
|
||||
try {
|
||||
// You need to create this CRUD method
|
||||
var response = await CRUD().post(
|
||||
link: AppLink.getArgumentAfterAppliedFromBackground,
|
||||
payload: {'driver_id': driverId},
|
||||
);
|
||||
Log.print('response: ${response}');
|
||||
Log.print('response: $response');
|
||||
|
||||
// Assuming the server returns order data if found, or 'failure'/'none' if not
|
||||
if (response['status'] == 'success') {
|
||||
final Map<String, dynamic> orderInfoFromServer = response['message'];
|
||||
final Map<String, dynamic> rideArguments =
|
||||
_transformServerDataToAppArguments(orderInfoFromServer);
|
||||
// 2. Build the new arguments map, matching your Flutter structure
|
||||
final Map<String, dynamic> orderInfo = response['message'];
|
||||
final Map<String, dynamic> rideArgs =
|
||||
_transformServerDataToAppArguments(orderInfo);
|
||||
|
||||
/////////////
|
||||
final customerToken = (response)['message']['token_passenger'];
|
||||
final orderId = (response)['message']['ride_id'].toString();
|
||||
box.write(BoxName.rideArgumentsFromBackground, rideArguments);
|
||||
final customerToken = response['message']['token_passenger'];
|
||||
final orderId = response['message']['ride_id'].toString();
|
||||
|
||||
box.write(BoxName.rideArgumentsFromBackground, rideArgs);
|
||||
box.write(BoxName.statusDriverLocation, 'on');
|
||||
box.write(BoxName.rideStatus, 'Apply');
|
||||
Get.put(OrderRequestController()).changeApplied();
|
||||
// MyDialog().getDialog(orderId.toString(), customerToken, () {});
|
||||
|
||||
// Now proceed with the UI flow
|
||||
// _sendAcceptanceNotification(customerToken, orderId.toString());
|
||||
// await _bringAppToForegroundAndNavigate(orderId);
|
||||
Get.to(() => PassengerLocationMapPage(),
|
||||
arguments: box.read(BoxName.rideArgumentsFromBackground));
|
||||
Get.to(
|
||||
() => PassengerLocationMapPage(),
|
||||
arguments: box.read(BoxName.rideArgumentsFromBackground),
|
||||
);
|
||||
} else {
|
||||
box.write(BoxName.rideArgumentsFromBackground, 'failure');
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
// silent
|
||||
} finally {
|
||||
_isProcessingOrder = false; // Release lock
|
||||
isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _transformServerDataToAppArguments(
|
||||
Map<String, dynamic> serverData) {
|
||||
// Helper function to safely get and convert values to String
|
||||
String _getString(String key, [String defaultValue = 'unknown']) {
|
||||
// serverData[key] might be an int, double, or string. .toString() handles all.
|
||||
// If it's null, use the default value.
|
||||
return serverData[key]?.toString() ?? defaultValue;
|
||||
}
|
||||
Map<String, dynamic> d) {
|
||||
String s(String key, [String def = 'unknown']) => d[key]?.toString() ?? def;
|
||||
|
||||
return {
|
||||
'passengerLocation': _getString('passenger_location'),
|
||||
'passengerDestination': _getString('passenger_destination'),
|
||||
'Duration': _getString('duration'),
|
||||
'totalCost': _getString('total_cost'),
|
||||
'Distance': _getString('distance'),
|
||||
'name': _getString('name'),
|
||||
'phone': _getString('phone'),
|
||||
'email': _getString('email'),
|
||||
'tokenPassenger': _getString('token_passenger'),
|
||||
'direction': _getString('direction_url'),
|
||||
'DurationToPassenger': _getString('duration_to_passenger'),
|
||||
'rideId': _getString('ride_id'),
|
||||
'passengerId': _getString('passenger_id'),
|
||||
'driverId': _getString('driver_id'),
|
||||
'durationOfRideValue': _getString('duration_of_ride'),
|
||||
'paymentAmount': _getString('payment_amount'),
|
||||
'paymentMethod': _getString('payment_method'),
|
||||
'passengerWalletBurc': _getString('passenger_wallet_burc'),
|
||||
'timeOfOrder': _getString('time_of_order'),
|
||||
'totalPassenger': _getString('total_passenger'),
|
||||
'carType': _getString('car_type'),
|
||||
'kazan': _getString('kazan'),
|
||||
'startNameLocation': _getString('start_name_location'),
|
||||
'endNameLocation': _getString('end_name_location'),
|
||||
|
||||
// --- Special Handling ---
|
||||
|
||||
// Steps (handle null values by providing an empty string)
|
||||
'step0': _getString('step0'),
|
||||
'step1': _getString('step1'),
|
||||
'step2': _getString('step2'),
|
||||
'step3': _getString('step3'),
|
||||
'step4': _getString('step4'),
|
||||
|
||||
// Boolean conversion (1/0 from server to 'true'/'false' string for the app)
|
||||
'WalletChecked': (serverData['wallet_checked'] == 1).toString(),
|
||||
|
||||
// Logic-based conversion for isHaveSteps
|
||||
// Your app's `rideArguments` expects 'startEnd', so we provide that if has_steps is 1.
|
||||
// You might need to adjust this logic if 'haveSteps' is also a possibility.
|
||||
'isHaveSteps': (serverData['has_steps'] == 1)
|
||||
? 'startEnd'
|
||||
: 'noSteps', // Providing a default
|
||||
'passengerLocation': s('passenger_location'),
|
||||
'passengerDestination': s('passenger_destination'),
|
||||
'Duration': s('duration'),
|
||||
'totalCost': s('total_cost'),
|
||||
'Distance': s('distance'),
|
||||
'name': s('name'),
|
||||
'phone': s('phone'),
|
||||
'email': s('email'),
|
||||
'tokenPassenger': s('token_passenger'),
|
||||
'direction': s('direction_url'),
|
||||
'DurationToPassenger': s('duration_to_passenger'),
|
||||
'rideId': s('ride_id'),
|
||||
'passengerId': s('passenger_id'),
|
||||
'driverId': s('driver_id'),
|
||||
'durationOfRideValue': s('duration_of_ride'),
|
||||
'paymentAmount': s('payment_amount'),
|
||||
'paymentMethod': s('payment_method'),
|
||||
'passengerWalletBurc': s('passenger_wallet_burc'),
|
||||
'timeOfOrder': s('time_of_order'),
|
||||
'totalPassenger': s('total_passenger'),
|
||||
'carType': s('car_type'),
|
||||
'kazan': s('kazan'),
|
||||
'startNameLocation': s('start_name_location'),
|
||||
'endNameLocation': s('end_name_location'),
|
||||
'step0': s('step0'),
|
||||
'step1': s('step1'),
|
||||
'step2': s('step2'),
|
||||
'step3': s('step3'),
|
||||
'step4': s('step4'),
|
||||
'WalletChecked': (d['wallet_checked'] == 1).toString(),
|
||||
'isHaveSteps': (d['has_steps'] == 1) ? 'startEnd' : 'noSteps',
|
||||
};
|
||||
}
|
||||
|
||||
void _sendAcceptanceNotification(String? customerToken, rideId) {
|
||||
try {
|
||||
if (customerToken == null) return;
|
||||
void _sendAcceptanceNotification(String? customerToken, dynamic rideId) {
|
||||
if (customerToken == null || customerToken.isEmpty) return;
|
||||
|
||||
List<String> bodyToPassenger = [
|
||||
box.read(BoxName.driverID).toString(),
|
||||
box.read(BoxName.nameDriver).toString(),
|
||||
box.read(BoxName.tokenDriver).toString(),
|
||||
rideId.toString()
|
||||
];
|
||||
List<String> body = [
|
||||
box.read(BoxName.driverID).toString(),
|
||||
box.read(BoxName.nameDriver).toString(),
|
||||
box.read(BoxName.tokenDriver).toString(),
|
||||
rideId.toString(),
|
||||
];
|
||||
|
||||
// Safely check for customer token
|
||||
final String? token = customerToken;
|
||||
if (token != null && token.isNotEmpty) {
|
||||
NotificationService.sendNotification(
|
||||
target: token.toString(),
|
||||
title: 'Accepted Ride'.tr,
|
||||
body: 'your ride is Accepted'.tr,
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'start',
|
||||
driverList: bodyToPassenger, category: 'Accepted Ride',
|
||||
);
|
||||
} else {}
|
||||
} catch (e) {}
|
||||
NotificationService.sendNotification(
|
||||
target: customerToken,
|
||||
title: 'Accepted Ride'.tr,
|
||||
body: 'your ride is Accepted'.tr,
|
||||
isTopic: false,
|
||||
tone: 'start',
|
||||
driverList: body,
|
||||
category: 'Accepted Ride',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
class ZonesController extends GetxController {
|
||||
Map<String, List<LatLng>> generateZoneMap(
|
||||
|
||||
@@ -132,10 +132,11 @@ class MaintainCenterPage extends StatelessWidget {
|
||||
"When you complete 600 trips, you will be eligible to receive offers for maintenance of your car."
|
||||
.tr,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
color: Colors.grey[700],
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.8),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Trip Count Section in a Card
|
||||
@@ -192,10 +193,11 @@ class MaintainCenterPage extends StatelessWidget {
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color: Colors.grey[800],
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.9),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:sefer_driver/constant/api_key.dart';
|
||||
|
||||
import '../../../../controller/functions/location_controller.dart';
|
||||
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||
@@ -21,76 +22,24 @@ class GoogleDriverMap extends StatelessWidget {
|
||||
final double mapPaddingBottom = MediaQuery.of(context).size.height * 0.3;
|
||||
|
||||
return GetBuilder<MapDriverController>(
|
||||
builder: (controller) => GoogleMap(
|
||||
builder: (controller) => IntaleqMap(
|
||||
apiKey: AK.mapAPIKEY,
|
||||
onMapCreated: (mapController) {
|
||||
controller.onMapCreated(mapController);
|
||||
// New: تطبيق الـ padding بعد إنشاء الخريطة مباشرة
|
||||
mapController.setMapStyle('[]'); // يمكنك إضافة تصميم مخصص للخريطة هنا
|
||||
// يمكنك استخدام CameraUpdate.scrollBy لتحريك الكاميرا إذا رغبت بذلك:
|
||||
// controller.mapController!.animateCamera(CameraUpdate.scrollBy(0, mapPaddingBottom));
|
||||
},
|
||||
// New: إضافة padding لتحريك مركز الخريطة للأعلى، مما يجعل أيقونة السائق تظهر في الأسفل
|
||||
|
||||
zoomControlsEnabled: false, // Changed: تم إخفاء أزرار الزوم الافتراضية
|
||||
zoomControlsEnabled: false,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: locationController.myLocation,
|
||||
zoom: 17,
|
||||
bearing: locationController.heading, // استخدام اتجاه السائق
|
||||
tilt: 60, // زاوية ميل
|
||||
bearing: locationController.heading,
|
||||
tilt: 60,
|
||||
),
|
||||
onCameraMove: (position) {
|
||||
CameraPosition(
|
||||
target: locationController.myLocation,
|
||||
zoom: 17.5,
|
||||
tilt: 50.0,
|
||||
bearing: locationController.heading,
|
||||
);
|
||||
},
|
||||
padding: EdgeInsets.only(bottom: 50, top: Get.height * 0.7),
|
||||
minMaxZoomPreference: const MinMaxZoomPreference(8, 18),
|
||||
myLocationEnabled: false, // Changed: تم الاعتماد على ماركر مخصص
|
||||
// padding: EdgeInsets.only(bottom: 50, top: Get.height * 0.7),
|
||||
// minMaxZoomPreference: const MinMaxZoomPreference(8, 18),
|
||||
myLocationEnabled: false,
|
||||
myLocationButtonEnabled: true,
|
||||
compassEnabled: true,
|
||||
mapType: MapType.terrain,
|
||||
trafficEnabled: true, // Changed: تفعيل عرض حركة المرور
|
||||
buildingsEnabled: true,
|
||||
polylines: {
|
||||
// Polyline(
|
||||
// zIndex: 2,
|
||||
|
||||
// polylineId: const PolylineId('route1'),
|
||||
// points: controller.polylineCoordinates,
|
||||
// color: const Color.fromARGB(255, 163, 81, 246),
|
||||
// width: 6, // Changed: زيادة عرض الخط
|
||||
// startCap: Cap.roundCap,
|
||||
// endCap: Cap.roundCap,
|
||||
// ),
|
||||
// Polyline(
|
||||
// zIndex: 2,
|
||||
|
||||
// polylineId: const PolylineId('route'),
|
||||
// points: controller.polylineCoordinatesDestination,
|
||||
// color: const Color.fromARGB(255, 10, 29, 126),
|
||||
// width: 6, // Changed: زيادة عرض الخط
|
||||
// startCap: Cap.roundCap,
|
||||
// endCap: Cap.roundCap,
|
||||
// ),
|
||||
Polyline(
|
||||
polylineId: const PolylineId('upcoming_route'),
|
||||
points: controller.upcomingPathPoints,
|
||||
color: Colors.blue, // أو أي لون آخر تختاره للمسار
|
||||
width: 8,
|
||||
zIndex: 2,
|
||||
),
|
||||
// 2. الخط المقطوع (تحت)
|
||||
Polyline(
|
||||
polylineId: const PolylineId('traveled_route'),
|
||||
points: controller.traveledPathPoints,
|
||||
color: Colors.grey.withOpacity(0.8),
|
||||
width: 7,
|
||||
zIndex: 1,
|
||||
),
|
||||
},
|
||||
polylines: controller.polyLines.toSet(),
|
||||
markers: {
|
||||
Marker(
|
||||
markerId: MarkerId('MyLocation'.tr),
|
||||
|
||||
@@ -48,7 +48,7 @@ class GoogleMapApp extends StatelessWidget {
|
||||
border: Border.all(
|
||||
color: AppColor.blueColor.withOpacity(0.2), width: 1),
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
MaterialCommunityIcons.google_maps,
|
||||
size: 28,
|
||||
color: AppColor.secondaryColor,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class MarkerGenerator {
|
||||
// دالة لرسم ماركر يحتوي على نص (للوقت والمسافة)
|
||||
static Future<BitmapDescriptor> createCustomMarkerBitmap({
|
||||
static Future<InlqBitmap> createCustomMarkerBitmap({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required Color color,
|
||||
@@ -19,15 +19,15 @@ class MarkerGenerator {
|
||||
const double height = 110.0;
|
||||
const double circleRadius = 25.0;
|
||||
|
||||
// 1. رسم المربع (Info Box)
|
||||
// 1. رسم المربع (Info Box) مع تدرج لوني بسيط
|
||||
final Paint paint = Paint()..color = color;
|
||||
final RRect rRect = RRect.fromRectAndRadius(
|
||||
const Rect.fromLTWH(0, 0, width, 60),
|
||||
const Radius.circular(15),
|
||||
const Rect.fromLTWH(0, 0, width, 65),
|
||||
const Radius.circular(20), // زوايا أكثر استدارة لشكل عصري
|
||||
);
|
||||
|
||||
// ظل خفيف
|
||||
canvas.drawShadow(Path()..addRRect(rRect), Colors.black, 5.0, true);
|
||||
// ظل أقوى لمظهر بارز (Premium Feel)
|
||||
canvas.drawShadow(Path()..addRRect(rRect), Colors.black54, 10.0, true);
|
||||
canvas.drawRRect(rRect, paint);
|
||||
|
||||
// 2. رسم مثلث صغير أسفل المربع (Arrow Tail)
|
||||
@@ -96,11 +96,11 @@ class MarkerGenerator {
|
||||
);
|
||||
final ByteData? data =
|
||||
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return BitmapDescriptor.fromBytes(data!.buffer.asUint8List());
|
||||
return InlqBitmap.fromBytes(data!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
// دالة خاصة لرسم ماركر السائق (دائرة وخلفها سهم)
|
||||
static Future<BitmapDescriptor> createDriverMarker() async {
|
||||
static Future<InlqBitmap> createDriverMarker() async {
|
||||
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
|
||||
final Canvas canvas = Canvas(pictureRecorder);
|
||||
const double size = 100.0;
|
||||
@@ -137,6 +137,6 @@ class MarkerGenerator {
|
||||
.toImage(size.toInt(), size.toInt());
|
||||
final ByteData? data =
|
||||
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return BitmapDescriptor.fromBytes(data!.buffer.asUint8List());
|
||||
return InlqBitmap.fromBytes(data!.buffer.asUint8List());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:sefer_driver/constant/api_key.dart';
|
||||
import 'package:sefer_driver/constant/colors.dart';
|
||||
import 'package:sefer_driver/controller/home/captin/order_request_controller.dart';
|
||||
|
||||
@@ -63,20 +64,21 @@ class OrderRequestPage extends StatelessWidget {
|
||||
// 1. الخارطة
|
||||
Positioned.fill(
|
||||
bottom: 300,
|
||||
child: GoogleMap(
|
||||
mapType: MapType.normal,
|
||||
child: IntaleqMap(
|
||||
apiKey: AK.mapAPIKEY,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: LatLng(
|
||||
controller.latPassenger, controller.lngPassenger),
|
||||
zoom: 13.0,
|
||||
),
|
||||
markers: controller.markers,
|
||||
mapType: Get.isDarkMode
|
||||
? IntaleqMapType.normal
|
||||
: IntaleqMapType.light,
|
||||
polylines: controller.polylines,
|
||||
zoomControlsEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
compassEnabled: false,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 80, bottom: 20, left: 20, right: 20),
|
||||
onMapCreated: (c) {
|
||||
controller.onMapCreated(c);
|
||||
controller.update();
|
||||
@@ -124,15 +126,16 @@ class OrderRequestPage extends StatelessWidget {
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
height: 360,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(25),
|
||||
topRight: Radius.circular(25),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
color:
|
||||
Theme.of(context).shadowColor.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 5)
|
||||
],
|
||||
@@ -146,8 +149,9 @@ class OrderRequestPage extends StatelessWidget {
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
color: Theme.of(context).dividerColor,
|
||||
borderRadius: BorderRadius.circular(2)))),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// الصف الأول: الراكب والسعر
|
||||
@@ -156,11 +160,14 @@ class OrderRequestPage extends StatelessWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: Color(0xFFF5F5F5),
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant,
|
||||
child: Icon(Icons.person,
|
||||
color: Colors.grey, size: 28),
|
||||
color: Theme.of(context).hintColor,
|
||||
size: 28),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
@@ -233,27 +240,45 @@ class OrderRequestPage extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F9FA),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant
|
||||
.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildInfoItem(
|
||||
carIcon, carTypeLabel, carTypeColor),
|
||||
context, carIcon, carTypeLabel, carTypeColor),
|
||||
Container(
|
||||
height: 20,
|
||||
width: 1,
|
||||
color: Colors.grey.shade300),
|
||||
_buildInfoItem(Icons.route,
|
||||
controller.totalTripDistance, Colors.black87),
|
||||
color: Theme.of(context).dividerColor),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
Icons.route,
|
||||
controller.totalTripDistance,
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.color ??
|
||||
Colors.black87),
|
||||
Container(
|
||||
height: 20,
|
||||
width: 1,
|
||||
color: Colors.grey.shade300),
|
||||
_buildInfoItem(Icons.access_time_filled,
|
||||
controller.totalTripDuration, Colors.black87),
|
||||
color: Theme.of(context).dividerColor),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
Icons.access_time_filled,
|
||||
controller.totalTripDuration,
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.color ??
|
||||
Colors.black87),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -397,7 +422,8 @@ class OrderRequestPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoItem(IconData icon, String text, Color color) {
|
||||
Widget _buildInfoItem(
|
||||
BuildContext context, IconData icon, String text, Color color) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
// import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
// import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart';
|
||||
// import 'package:sefer_driver/constant/box_name.dart';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:sefer_driver/constant/api_key.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../../../../constant/colors.dart';
|
||||
@@ -60,7 +61,8 @@ class _OrderRequestPageTestState extends State<OrderRequestPageTest> {
|
||||
children: [
|
||||
SizedBox(
|
||||
height: Get.height * .33,
|
||||
child: Obx(() => GoogleMap(
|
||||
child: Obx(() => IntaleqMap(
|
||||
apiKey: AK.mapAPIKEY,
|
||||
initialCameraPosition: CameraPosition(
|
||||
zoom: 12,
|
||||
target:
|
||||
@@ -69,13 +71,13 @@ class _OrderRequestPageTestState extends State<OrderRequestPageTest> {
|
||||
cameraTargetBounds: CameraTargetBounds(
|
||||
_orderRequestController.mapBounds.value),
|
||||
myLocationButtonEnabled: true,
|
||||
trafficEnabled: false,
|
||||
buildingsEnabled: false,
|
||||
mapToolbarEnabled: true,
|
||||
// trafficEnabled: false,
|
||||
// buildingsEnabled: false,
|
||||
// mapToolbarEnabled: true,
|
||||
myLocationEnabled: true,
|
||||
markers: _orderRequestController.markers.value,
|
||||
polylines: _orderRequestController.polylines.value,
|
||||
onMapCreated: (GoogleMapController controller) {
|
||||
onMapCreated: (IntaleqMapController controller) {
|
||||
_orderRequestController.mapController.value =
|
||||
controller;
|
||||
},
|
||||
@@ -104,11 +106,11 @@ class OrderRequestController extends GetxController {
|
||||
Rx<Set<Marker>> markers = Rx<Set<Marker>>({});
|
||||
Rx<Set<Polyline>> polylines = Rx<Set<Polyline>>({});
|
||||
|
||||
Rx<GoogleMapController?> mapController = Rx<GoogleMapController?>(null);
|
||||
Rx<IntaleqMapController?> mapController = Rx<IntaleqMapController?>(null);
|
||||
|
||||
// Icons for start and end markers
|
||||
late BitmapDescriptor startIcon;
|
||||
late BitmapDescriptor endIcon;
|
||||
late InlqBitmap startIcon;
|
||||
late InlqBitmap endIcon;
|
||||
|
||||
// Coordinates for passenger location and destination
|
||||
Rx<LatLng?> passengerLocation = Rx<LatLng?>(null);
|
||||
@@ -123,12 +125,8 @@ class OrderRequestController extends GetxController {
|
||||
|
||||
void _initializeMarkerIcons() async {
|
||||
// Load custom marker icons
|
||||
startIcon = await BitmapDescriptor.fromAssetImage(
|
||||
const ImageConfiguration(size: Size(48, 48)),
|
||||
'assets/start_marker.png');
|
||||
|
||||
endIcon = await BitmapDescriptor.fromAssetImage(
|
||||
const ImageConfiguration(size: Size(48, 48)), 'assets/end_marker.png');
|
||||
startIcon = InlqBitmap.fromAsset('assets/start_marker.png');
|
||||
endIcon = InlqBitmap.fromAsset('assets/end_marker.png');
|
||||
}
|
||||
|
||||
void parseCoordinates(List myList) {
|
||||
@@ -184,10 +182,10 @@ class OrderRequestController extends GetxController {
|
||||
polylines.value = {
|
||||
Polyline(
|
||||
zIndex: 1,
|
||||
consumeTapEvents: true,
|
||||
// consumeTapEvents: true,
|
||||
geodesic: true,
|
||||
endCap: Cap.buttCap,
|
||||
startCap: Cap.buttCap,
|
||||
// endCap: Cap.buttCap,
|
||||
// startCap: Cap.buttCap,
|
||||
visible: true,
|
||||
polylineId: const PolylineId('routeOrder'),
|
||||
points: [passengerLocation.value!, passengerDestination.value!],
|
||||
|
||||
@@ -2,30 +2,35 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:sefer_driver/constant/colors.dart';
|
||||
import 'package:sefer_driver/constant/style.dart'; // Assuming this has your text styles
|
||||
import 'package:sefer_driver/views/widgets/mycircular.dart'; // Assuming this is your loading widget
|
||||
|
||||
import 'package:sefer_driver/constant/style.dart';
|
||||
import 'package:sefer_driver/constant/finance_design_system.dart';
|
||||
import 'package:sefer_driver/views/widgets/mycircular.dart';
|
||||
import '../../../controller/payment/driver_payment_controller.dart';
|
||||
import 'widgets/transaction_preview_item.dart';
|
||||
|
||||
class PaymentHistoryDriverPage extends StatelessWidget {
|
||||
const PaymentHistoryDriverPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Initialize your controller
|
||||
Get.put(DriverWalletHistoryController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Payment History'.tr),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 1,
|
||||
title: Text('Payment History'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded, color: FinanceDesignSystem.primaryDark, size: 20),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.grey[100],
|
||||
body: GetBuilder<DriverWalletHistoryController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
// Using your custom loading indicator
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
@@ -34,16 +39,10 @@ class PaymentHistoryDriverPage extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.account_balance_wallet_outlined,
|
||||
size: 80, color: Colors.grey[400]),
|
||||
Icon(Icons.history_rounded, size: 80, color: Colors.grey.shade300),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No transactions yet'.tr,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(color: Colors.grey[600]),
|
||||
),
|
||||
Text('No transactions yet'.tr,
|
||||
style: TextStyle(color: Colors.grey.shade400, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -51,18 +50,26 @@ class PaymentHistoryDriverPage extends StatelessWidget {
|
||||
|
||||
return AnimationLimiter(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
itemCount: controller.archive.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var transaction = controller.archive[index];
|
||||
|
||||
final tx = controller.archive[index];
|
||||
final double amount = double.tryParse(tx['amount']?.toString() ?? '0') ?? 0;
|
||||
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: _TransactionCard(transaction: transaction),
|
||||
child: TransactionPreviewItem(
|
||||
title: amount >= 0 ? 'Credit'.tr : 'Debit'.tr,
|
||||
subtitle: tx['created_at'] ?? '',
|
||||
amount: amount.abs().toStringAsFixed(0),
|
||||
date: tx['created_at']?.split(' ')[0] ?? '',
|
||||
type: amount >= 0 ? 'credit' : 'debit',
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -74,71 +81,3 @@ class PaymentHistoryDriverPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A dedicated widget for displaying a single transaction with a modern UI.
|
||||
class _TransactionCard extends StatelessWidget {
|
||||
final Map<String, dynamic> transaction;
|
||||
|
||||
const _TransactionCard({required this.transaction});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Safely parse the amount to avoid errors
|
||||
final double amount =
|
||||
double.tryParse(transaction['amount']?.toString() ?? '0') ?? 0;
|
||||
|
||||
final bool isCredit = amount >= 0;
|
||||
|
||||
final Color indicatorColor =
|
||||
isCredit ? AppColor.greenColor : AppColor.redColor;
|
||||
final IconData iconData =
|
||||
isCredit ? Icons.arrow_upward_rounded : Icons.arrow_downward_rounded;
|
||||
final String transactionType = (isCredit ? 'Credit'.tr : 'Debit'.tr).tr;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withOpacity(0.05),
|
||||
margin: const EdgeInsets.only(bottom: 12.0),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
clipBehavior: Clip.antiAlias, // Ensures the color bar is clipped neatly
|
||||
child: IntrinsicHeight(
|
||||
// Ensures the color bar and content have the same height
|
||||
child: Row(
|
||||
children: [
|
||||
// Left-side color indicator bar
|
||||
Container(width: 6, color: indicatorColor),
|
||||
|
||||
Expanded(
|
||||
child: ListTile(
|
||||
leading: Icon(iconData, color: indicatorColor, size: 30),
|
||||
title: Text(
|
||||
// Use .abs() to remove the negative sign from the display
|
||||
'${amount.abs().toStringAsFixed(2)} ${'SYP'.tr}',
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
transaction['created_at'] ?? 'No date',
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
trailing: Text(
|
||||
transactionType,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 14,
|
||||
color: indicatorColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:sefer_driver/controller/payment/smsPaymnet/payment_services.dart
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/finance_design_system.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../controller/payment/mtn_new/mtn_payment_new_screen.dart';
|
||||
@@ -21,8 +22,8 @@ import '../../widgets/my_textField.dart';
|
||||
import 'ecash.dart';
|
||||
|
||||
class PointsCaptain extends StatelessWidget {
|
||||
PaymentController paymentController = Get.put(PaymentController());
|
||||
CaptainWalletController captainWalletController =
|
||||
final PaymentController paymentController = Get.put(PaymentController());
|
||||
final CaptainWalletController captainWalletController =
|
||||
Get.put(CaptainWalletController());
|
||||
|
||||
PointsCaptain({
|
||||
@@ -31,263 +32,67 @@ class PointsCaptain extends StatelessWidget {
|
||||
required this.countPoint,
|
||||
required this.pricePoint,
|
||||
});
|
||||
|
||||
final Color kolor;
|
||||
final String countPoint;
|
||||
double pricePoint;
|
||||
final double pricePoint;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
Get.defaultDialog(
|
||||
title: 'Which method you will pay'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 12, bottom: 4),
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
elevation: 4,
|
||||
shadowColor: kolor.withOpacity(0.3),
|
||||
child: InkWell(
|
||||
onTap: () => _showPaymentOptions(context),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
width: 130,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
kolor.withOpacity(0.05),
|
||||
Colors.white,
|
||||
],
|
||||
),
|
||||
border: Border.all(color: kolor.withOpacity(0.2), width: 1.5),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'${'you can buy '.tr}$countPoint ${'L.S'.tr}${'by '.tr}${'$pricePoint'.tr}',
|
||||
style: AppStyle.title,
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: kolor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.account_balance_wallet_rounded,
|
||||
color: kolor, size: 24),
|
||||
),
|
||||
|
||||
// Add some spacing between buttons
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
payWithEcashDriver(context, pricePoint.toString());
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Pay with Debit Card'.tr),
|
||||
const SizedBox(width: 10),
|
||||
Icon(Icons.credit_card_sharp,
|
||||
color: AppColor.blueColor, size: 70),
|
||||
],
|
||||
)),
|
||||
// GestureDetector(
|
||||
// onTap: () async {
|
||||
// Get.back();
|
||||
// Get.defaultDialog(
|
||||
// barrierDismissible: false,
|
||||
// title: 'Insert Wallet phone number'.tr,
|
||||
// content: Form(
|
||||
// key: paymentController.formKey,
|
||||
// child: MyTextForm(
|
||||
// controller:
|
||||
// paymentController.walletphoneController,
|
||||
// label: 'Insert Wallet phone number'.tr,
|
||||
// hint: '963941234567',
|
||||
// type: TextInputType.phone)),
|
||||
// confirm: MyElevatedButton(
|
||||
// title: 'OK'.tr,
|
||||
// onPressed: () async {
|
||||
// Get.back();
|
||||
// if (paymentController.formKey.currentState!
|
||||
// .validate()) {
|
||||
// box.write(
|
||||
// BoxName.phoneWallet,
|
||||
// paymentController
|
||||
// .walletphoneController.text);
|
||||
// await payWithMTNWallet(
|
||||
// context, pricePoint.toString(), 'SYP');
|
||||
// }
|
||||
// }));
|
||||
// },
|
||||
// child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Text('Pay by MTN Wallet'.tr),
|
||||
// const SizedBox(width: 10),
|
||||
// Image.asset(
|
||||
// 'assets/images/cashMTN.png',
|
||||
// width: 70,
|
||||
// height: 70,
|
||||
// fit: BoxFit.fill,
|
||||
// ),
|
||||
// ],
|
||||
// )),
|
||||
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'Insert Wallet phone number'.tr,
|
||||
content: Form(
|
||||
key: paymentController.formKey,
|
||||
child: MyTextForm(
|
||||
controller:
|
||||
paymentController.walletphoneController,
|
||||
label: 'Insert Wallet phone number'.tr,
|
||||
hint: '963991234567',
|
||||
type: TextInputType.phone)),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'OK'.tr,
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
if (paymentController.formKey.currentState!
|
||||
.validate()) {
|
||||
box.write(
|
||||
BoxName.phoneWallet,
|
||||
paymentController
|
||||
.walletphoneController.text);
|
||||
await payWithSyriaTelWallet(
|
||||
context, pricePoint.toString(), 'SYP');
|
||||
}
|
||||
}));
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Pay by Syriatel Wallet'.tr),
|
||||
const SizedBox(width: 10),
|
||||
Image.asset(
|
||||
'assets/images/syriatel.jpeg',
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
],
|
||||
)),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
// التحقق بالبصمة قبل أي شيء
|
||||
bool isAuthSupported =
|
||||
await LocalAuthentication().isDeviceSupported();
|
||||
|
||||
if (isAuthSupported) {
|
||||
bool didAuthenticate =
|
||||
await LocalAuthentication().authenticate(
|
||||
localizedReason:
|
||||
'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
|
||||
);
|
||||
|
||||
if (!didAuthenticate) {
|
||||
print("❌ User did not authenticate with biometrics");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// الانتقال مباشرة لإنشاء الفاتورة بعد النجاح بالبصمة
|
||||
Get.to(
|
||||
() => PaymentScreenSmsProvider(amount: pricePoint));
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Pay by Sham Cash'.tr),
|
||||
const SizedBox(width: 10),
|
||||
Image.asset(
|
||||
'assets/images/shamCash.png',
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
],
|
||||
)),
|
||||
// GestureDetector(
|
||||
// onTap: () async {
|
||||
// Get.back();
|
||||
// Get.defaultDialog(
|
||||
// barrierDismissible: false,
|
||||
// title: 'Insert Wallet phone number'.tr,
|
||||
// content: Form(
|
||||
// key: paymentController.formKey,
|
||||
// child: MyTextForm(
|
||||
// controller:
|
||||
// paymentController.walletphoneController,
|
||||
// label: 'Insert Wallet phone number'.tr,
|
||||
// hint: '963941234567',
|
||||
// type: TextInputType.phone)),
|
||||
// confirm: MyElevatedButton(
|
||||
// title: 'OK'.tr,
|
||||
// onPressed: () async {
|
||||
// Get.back();
|
||||
// if (paymentController.formKey.currentState!
|
||||
// .validate()) {
|
||||
// box.write(
|
||||
// BoxName.phoneWallet,
|
||||
// paymentController
|
||||
// .walletphoneController.text);
|
||||
// // await payWithSyriaTelWallet(
|
||||
// // context, pricePoint.toString(), 'SYP');
|
||||
// bool isAuthSupported =
|
||||
// await LocalAuthentication()
|
||||
// .isDeviceSupported();
|
||||
// if (isAuthSupported) {
|
||||
// bool didAuthenticate =
|
||||
// await LocalAuthentication()
|
||||
// .authenticate(
|
||||
// localizedReason:
|
||||
// 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
|
||||
// );
|
||||
// if (!didAuthenticate) {
|
||||
// if (Get.isDialogOpen ?? false) Get.back();
|
||||
// print(
|
||||
// "❌ User did not authenticate with biometrics");
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// Get.to(() => PaymentScreenMtn(
|
||||
// amount: pricePoint,
|
||||
// userType: 'Driver',
|
||||
// ));
|
||||
// }
|
||||
// }));
|
||||
// },
|
||||
// child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Text('Pay by MTN Wallet'.tr),
|
||||
// const SizedBox(width: 10),
|
||||
// Image.asset(
|
||||
// 'assets/images/cashMTN.png',
|
||||
// width: 70,
|
||||
// height: 70,
|
||||
// fit: BoxFit.fill,
|
||||
// ),
|
||||
// ],
|
||||
// )),
|
||||
],
|
||||
));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 8),
|
||||
child: Container(
|
||||
width: Get.width * .22,
|
||||
height: Get.width * .22,
|
||||
margin: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
kolor.withOpacity(0.3),
|
||||
kolor,
|
||||
kolor.withOpacity(0.7),
|
||||
kolor,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
border: Border.all(color: AppColor.accentColor),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
shape: BoxShape.rectangle,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'$countPoint ${'L.S'.tr}',
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(color: AppColor.secondaryColor),
|
||||
'$countPoint ${'SYP'.tr}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 15,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$pricePoint ${'L.S'.tr}',
|
||||
style:
|
||||
AppStyle.title.copyWith(color: AppColor.secondaryColor),
|
||||
textAlign: TextAlign.center,
|
||||
'${'Price:'.tr} ${pricePoint.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -296,6 +101,183 @@ class PointsCaptain extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPaymentOptions(BuildContext context) {
|
||||
Get.bottomSheet(
|
||||
isScrollControlled: true,
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.8,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(32)),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("Select Payment Method".tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close_rounded, color: Colors.grey),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"${'Amount to charge:'.tr} $countPoint ${'SYP'.tr}",
|
||||
style: FinanceDesignSystem.subHeadingStyle),
|
||||
const SizedBox(height: 24),
|
||||
_buildPaymentMethodTile(
|
||||
icon: Icons.credit_card_rounded,
|
||||
title: 'Debit Card'.tr,
|
||||
subtitle: 'E-Cash payment gateway'.tr,
|
||||
color: Colors.blue,
|
||||
onTap: () {
|
||||
Get.back();
|
||||
payWithEcashDriver(context, pricePoint.toString());
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildPaymentMethodTile(
|
||||
image: 'assets/images/syriatel.jpeg',
|
||||
title: 'Syriatel Cash'.tr,
|
||||
subtitle: 'Pay using Syriatel mobile wallet'.tr,
|
||||
color: Colors.red,
|
||||
onTap: () => _showPhoneInputDialog(context, 'Syriatel'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildPaymentMethodTile(
|
||||
image: 'assets/images/shamCash.png',
|
||||
title: 'Sham Cash'.tr,
|
||||
subtitle: 'Pay using Sham Cash wallet'.tr,
|
||||
color: Colors.orange,
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
bool isAuthSupported =
|
||||
await LocalAuthentication().isDeviceSupported();
|
||||
if (isAuthSupported) {
|
||||
bool didAuthenticate =
|
||||
await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Confirm payment with biometrics'.tr,
|
||||
);
|
||||
if (!didAuthenticate) return;
|
||||
}
|
||||
Get.to(() => PaymentScreenSmsProvider(amount: pricePoint));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentMethodTile({
|
||||
IconData? icon,
|
||||
String? image,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.grey.shade200, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: image != null
|
||||
? Image.asset(image, fit: BoxFit.contain)
|
||||
: Icon(icon, color: color, size: 30),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
const SizedBox(height: 2),
|
||||
Text(subtitle,
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios_rounded,
|
||||
size: 14, color: Colors.grey.shade400),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPhoneInputDialog(BuildContext context, String provider) {
|
||||
Get.back();
|
||||
Get.defaultDialog(
|
||||
title: 'Wallet Phone Number'.tr,
|
||||
content: Form(
|
||||
key: paymentController.formKey,
|
||||
child: MyTextForm(
|
||||
controller: paymentController.walletphoneController,
|
||||
label: 'Phone Number'.tr,
|
||||
hint: provider == 'Syriatel' ? '963991234567' : '963941234567',
|
||||
type: TextInputType.phone,
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Confirm'.tr,
|
||||
onPressed: () async {
|
||||
if (paymentController.formKey.currentState!.validate()) {
|
||||
Get.back();
|
||||
box.write(BoxName.phoneWallet,
|
||||
paymentController.walletphoneController.text);
|
||||
if (provider == 'Syriatel') {
|
||||
await payWithSyriaTelWallet(
|
||||
context, pricePoint.toString(), 'SYP');
|
||||
} else {
|
||||
await payWithMTNWallet(context, pricePoint.toString(), 'SYP');
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentScreen extends StatefulWidget {
|
||||
|
||||
@@ -1,521 +1,370 @@
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:sefer_driver/constant/links.dart';
|
||||
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||
import 'package:sefer_driver/controller/functions/tts.dart';
|
||||
import 'package:sefer_driver/views/home/my_wallet/payment_history_driver_page.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/constant/colors.dart';
|
||||
import 'package:sefer_driver/constant/info.dart';
|
||||
import 'package:sefer_driver/constant/style.dart';
|
||||
import 'package:sefer_driver/constant/finance_design_system.dart';
|
||||
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
|
||||
import 'package:sefer_driver/main.dart';
|
||||
import 'package:sefer_driver/views/widgets/mycircular.dart';
|
||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:sefer_driver/views/widgets/my_textField.dart';
|
||||
import 'package:sefer_driver/views/widgets/mycircular.dart';
|
||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/constant/links.dart';
|
||||
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||
import 'package:sefer_driver/main.dart';
|
||||
import 'package:sefer_driver/views/home/my_wallet/payment_history_driver_page.dart';
|
||||
import 'package:sefer_driver/controller/payment/driver_payment_controller.dart';
|
||||
|
||||
import '../../../controller/payment/driver_payment_controller.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import 'card_wallet_widget.dart';
|
||||
// Import new widgets
|
||||
import 'points_captain.dart';
|
||||
import 'transfer_budget_page.dart';
|
||||
import 'weekly_payment_page.dart';
|
||||
import 'widgets/balance_card.dart';
|
||||
import 'widgets/quick_actions.dart';
|
||||
import 'widgets/financial_summary_card.dart';
|
||||
import 'widgets/promo_gamification_card.dart';
|
||||
import 'widgets/transaction_preview_item.dart';
|
||||
|
||||
class WalletCaptainRefactored extends StatelessWidget {
|
||||
WalletCaptainRefactored({super.key});
|
||||
|
||||
final CaptainWalletController captainWalletController =
|
||||
Get.put(CaptainWalletController());
|
||||
|
||||
// دالة مساعدة لتحديد لون خلفية النقاط
|
||||
Color _getPointsColor(String pointsStr) {
|
||||
final points = double.tryParse(pointsStr) ?? 0.0;
|
||||
if (points < -30000) {
|
||||
return AppColor.redColor;
|
||||
} else if (points < 0 && points >= -30000) {
|
||||
return AppColor.yellowColor;
|
||||
} else {
|
||||
return AppColor.greenColor;
|
||||
}
|
||||
}
|
||||
final CaptainWalletController controller = Get.put(CaptainWalletController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
captainWalletController.refreshCaptainWallet();
|
||||
return MyScafolld(
|
||||
title: 'Driver Balance'.tr,
|
||||
isleading: true,
|
||||
action: IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () => captainWalletController.refreshCaptainWallet(),
|
||||
tooltip: 'Refresh'.tr,
|
||||
controller.refreshCaptainWallet();
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Driver Balance'.tr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: FinanceDesignSystem.primaryDark, size: 20),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh_rounded,
|
||||
color: FinanceDesignSystem.primaryDark),
|
||||
onPressed: () => controller.refreshCaptainWallet(),
|
||||
tooltip: 'Refresh'.tr,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: [
|
||||
GetBuilder<CaptainWalletController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return const MyCircularProgressIndicator();
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildTotalPointsSection(context, controller),
|
||||
const SizedBox(height: 16),
|
||||
const CardSeferWalletDriver(), // This can be redesigned if needed
|
||||
const SizedBox(height: 16),
|
||||
_buildWalletDetailsCard(context, controller),
|
||||
const SizedBox(height: 24),
|
||||
_buildPromoSection(controller),
|
||||
const SizedBox(height: 24),
|
||||
_buildNavigationButtons(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// القسم العلوي لعرض النقاط الإجمالية
|
||||
Widget _buildTotalPointsSection(
|
||||
BuildContext context, CaptainWalletController controller) {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
color: _getPointsColor(controller.totalPoints.toString()),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => CupertinoAlertDialog(
|
||||
title: Text('Info'.tr),
|
||||
content: Text(
|
||||
'The 30000 points equal 30000 S.P for you \nSo go and gain your money'
|
||||
.tr),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text("OK".tr),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
body: GetBuilder<CaptainWalletController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: FinanceDesignSystem.horizontalPadding,
|
||||
vertical: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 1. Header / Balance
|
||||
BalanceCard(
|
||||
balance: controller.totalPoints.toString(),
|
||||
isNegative:
|
||||
double.tryParse(controller.totalPoints.toString()) !=
|
||||
null &&
|
||||
double.parse(controller.totalPoints.toString()) <
|
||||
-30000,
|
||||
lastUpdated: "Just now".tr,
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 2. Quick Actions
|
||||
QuickActionsGrid(
|
||||
onAddBalance: () =>
|
||||
_showAddBalanceOptions(context, controller),
|
||||
onWithdraw: () => addSyrianPaymentMethod(controller),
|
||||
onTransfer: () => Get.to(() => TransferBudgetPage()),
|
||||
onHistory: () async {
|
||||
await Get.put(DriverWalletHistoryController())
|
||||
.getArchivePayment();
|
||||
Get.to(() => const PaymentHistoryDriverPage());
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 3. Earnings Summary
|
||||
FinancialSummaryCard(
|
||||
title: 'Earnings Summary'.tr,
|
||||
subtitle: 'ملخص الأرباح'.tr,
|
||||
items: [
|
||||
SummaryItem(
|
||||
icon: Icons.money_rounded,
|
||||
label: 'Cash Earnings'.tr,
|
||||
amount: controller.totalAmount,
|
||||
color: FinanceDesignSystem.successGreen,
|
||||
),
|
||||
SummaryItem(
|
||||
icon: Icons.credit_card_rounded,
|
||||
label: 'Card Earnings'.tr,
|
||||
amount: controller.totalAmountVisa,
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 3. Recharge Balance Packages
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Recharge Balance'.tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
Icon(Icons.info_outline_rounded,
|
||||
size: 18, color: Colors.grey.shade400),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 140, // Increased height for modern cards
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: [
|
||||
PointsCaptain(
|
||||
kolor: Colors.blueGrey,
|
||||
pricePoint: 100,
|
||||
countPoint: '100'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.brown,
|
||||
pricePoint: 200,
|
||||
countPoint: '210'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.amber,
|
||||
pricePoint: 400,
|
||||
countPoint: '450'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.orange,
|
||||
pricePoint: 1000,
|
||||
countPoint: '1100'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 5. Promotions
|
||||
Text('Promotions'.tr, style: FinanceDesignSystem.headingStyle),
|
||||
const SizedBox(height: 12),
|
||||
PromoGamificationCard(
|
||||
title: 'Morning Promo'.tr,
|
||||
subtitle: "from 7:00am to 10:00am".tr,
|
||||
currentProgress: controller.walletDate['message']?[0]
|
||||
?['morning_count'] ??
|
||||
0,
|
||||
targetProgress: 5,
|
||||
reward: "+50 SYP",
|
||||
onTap: () =>
|
||||
controller.addDriverWalletFromPromo('Morning Promo', 50),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PromoGamificationCard(
|
||||
title: 'Afternoon Promo'.tr,
|
||||
subtitle: "from 3:00pm to 6:00 pm".tr,
|
||||
currentProgress: controller.walletDate['message']?[0]
|
||||
?['afternoon_count'] ??
|
||||
0,
|
||||
targetProgress: 5,
|
||||
reward: "+50 SYP",
|
||||
onTap: () => controller.addDriverWalletFromPromo(
|
||||
'Afternoon Promo', 50),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 6. Transactions Preview
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Recent Transactions'.tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await Get.put(DriverWalletHistoryController())
|
||||
.getArchivePayment();
|
||||
Get.to(() => const PaymentHistoryDriverPage());
|
||||
},
|
||||
child: Text('View All'.tr,
|
||||
style: const TextStyle(
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
GetBuilder<DriverWalletHistoryController>(
|
||||
init: DriverWalletHistoryController(),
|
||||
builder: (historyController) {
|
||||
if (historyController.archive.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: Center(
|
||||
child: Text("No transactions yet".tr,
|
||||
style: TextStyle(color: Colors.grey.shade400))),
|
||||
);
|
||||
}
|
||||
// Show only last 3
|
||||
final lastThree =
|
||||
historyController.archive.take(3).toList();
|
||||
return Column(
|
||||
children: lastThree.map((tx) {
|
||||
final double amount =
|
||||
double.tryParse(tx['amount']?.toString() ?? '0') ??
|
||||
0;
|
||||
return TransactionPreviewItem(
|
||||
title: amount >= 0 ? 'Credit'.tr : 'Debit'.tr,
|
||||
subtitle: tx['created_at'] ?? '',
|
||||
amount: amount.abs().toStringAsFixed(0),
|
||||
date: tx['created_at']?.split(' ')[0] ?? '',
|
||||
type: amount >= 0 ? 'credit' : 'debit',
|
||||
onTap: () {},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'رصيد التشغيل 💎',
|
||||
style: AppStyle.headTitle2.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
controller.totalPoints.toString(),
|
||||
style: AppStyle.headTitle2.copyWith(
|
||||
color: Colors.white,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (double.parse(controller.totalPoints.toString()) < -30000)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: CupertinoButton(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
onPressed: () {
|
||||
// Add your charge account logic here
|
||||
},
|
||||
child: Text(
|
||||
'Charge your Account'.tr,
|
||||
style: TextStyle(
|
||||
color: AppColor.redColor,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// بطاقة لعرض تفاصيل المحفظة وخيارات الشراء
|
||||
Widget _buildWalletDetailsCard(
|
||||
void _showAddBalanceOptions(
|
||||
BuildContext context, CaptainWalletController controller) {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_BudgetInfoRow(
|
||||
title: 'Total Budget from trips is '.tr,
|
||||
amount: controller.totalAmount,
|
||||
onTap: () {
|
||||
Get.snackbar(
|
||||
icon: InkWell(
|
||||
onTap: () async => await Get.put(TextToSpeechController())
|
||||
.speakText(
|
||||
'This amount for all trip I get from Passengers'
|
||||
.tr),
|
||||
child: const Icon(Icons.headphones)),
|
||||
'${'Total Amount:'.tr} ${controller.totalAmount} ${'SYP'.tr}',
|
||||
'This amount for all trip I get from Passengers'.tr,
|
||||
duration: const Duration(seconds: 6),
|
||||
backgroundColor: AppColor.yellowColor,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(height: 32),
|
||||
_BudgetInfoRow(
|
||||
title: 'Total Budget from trips by\nCredit card is '.tr,
|
||||
amount: controller.totalAmountVisa,
|
||||
onTap: () {
|
||||
Get.snackbar(
|
||||
icon: InkWell(
|
||||
onTap: () async => await Get.find<TextToSpeechController>()
|
||||
.speakText(
|
||||
'This amount for all trip I get from Passengers and Collected For me in'
|
||||
.tr +
|
||||
' Intaleq Wallet'.tr),
|
||||
child: const Icon(Icons.headphones),
|
||||
),
|
||||
'${'Total Amount:'.tr} ${controller.totalAmountVisa} ${'SYP'.tr}',
|
||||
'This amount for all trip I get from Passengers and Collected For me in'
|
||||
.tr +
|
||||
' ${AppInformation.appName} Wallet'.tr,
|
||||
duration: const Duration(seconds: 6),
|
||||
backgroundColor: AppColor.redColor,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildBuyPointsButton(controller),
|
||||
const SizedBox(height: 16),
|
||||
// _buildTransferBudgetButton(controller), // Uncomment if needed
|
||||
_buildPurchaseInstructions(),
|
||||
Text("Add Balance".tr, style: FinanceDesignSystem.headingStyle),
|
||||
const SizedBox(height: 8),
|
||||
_buildPointsOptions(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// قسم العروض الترويجية
|
||||
Widget _buildPromoSection(CaptainWalletController controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Today's Promo".tr, style: AppStyle.headTitle),
|
||||
const SizedBox(height: 10),
|
||||
_PromoProgressCard(
|
||||
title: 'Morning Promo'.tr,
|
||||
timePromo: 'Morning Promo',
|
||||
count: (controller.walletDate['message'][0]['morning_count']),
|
||||
maxCount: 5,
|
||||
description:
|
||||
"this is count of your all trips in the morning promo today from 7:00am to 10:00am"
|
||||
.tr,
|
||||
controller: controller,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_PromoProgressCard(
|
||||
title: 'Afternoon Promo'.tr,
|
||||
timePromo: 'Afternoon Promo',
|
||||
count: (controller.walletDate['message'][0]['afternoon_count']),
|
||||
maxCount: 5,
|
||||
description:
|
||||
"this is count of your all trips in the Afternoon promo today from 3:00pm to 6:00 pm"
|
||||
.tr,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// أزرار التنقل السفلية
|
||||
Widget _buildNavigationButtons() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyElevatedButton(
|
||||
kolor: AppColor.blueColor,
|
||||
title: 'Payment History'.tr,
|
||||
onPressed: () async {
|
||||
await Get.put(DriverWalletHistoryController())
|
||||
.getArchivePayment();
|
||||
Get.to(() => const PaymentHistoryDriverPage(),
|
||||
transition: Transition.size);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: MyElevatedButton(
|
||||
kolor: AppColor.blueColor,
|
||||
title: 'Weekly Budget'.tr,
|
||||
onPressed: () async {
|
||||
await Get.put(DriverWalletHistoryController())
|
||||
.getWeekllyArchivePayment();
|
||||
Get.to(() => const WeeklyPaymentPage(),
|
||||
transition: Transition.size);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// --- حافظت على هذه الدوال كما هي لأنها تحتوي على منطق مهم ---
|
||||
Widget _buildBuyPointsButton(CaptainWalletController controller) {
|
||||
return MyElevatedButton(
|
||||
title: 'You can buy points from your budget'.tr,
|
||||
onPressed: () {
|
||||
Get.defaultDialog(
|
||||
title: 'Pay from my budget'.tr,
|
||||
content: Form(
|
||||
key: controller.formKey,
|
||||
child: MyTextForm(
|
||||
controller: controller.amountFromBudgetController,
|
||||
label:
|
||||
'${'You have in account'.tr} ${controller.totalAmountVisa}',
|
||||
hint: '${'You have in account'.tr} ${controller.totalAmountVisa}',
|
||||
type: TextInputType.number,
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Pay'.tr,
|
||||
onPressed: () async {
|
||||
bool isAvailable =
|
||||
await LocalAuthentication().isDeviceSupported();
|
||||
if (isAvailable) {
|
||||
// Authenticate the user
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason:
|
||||
'Use Touch ID or Face ID to confirm payment'.tr,
|
||||
// options: const AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
if (double.parse(controller.amountFromBudgetController.text) <
|
||||
double.parse(controller.totalAmountVisa)) {
|
||||
await controller.payFromBudget();
|
||||
} else {
|
||||
Get.back();
|
||||
|
||||
mySnackeBarError('Your Budget less than needed'.tr);
|
||||
}
|
||||
} else {
|
||||
// Authentication failed, handle accordingly
|
||||
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog('Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr,
|
||||
() {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Cancel'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPurchaseInstructions() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.accentColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColor.accentColor.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"You can purchase a budget to enable online access through the options listed below"
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title.copyWith(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPointsOptions() {
|
||||
return SizedBox(
|
||||
height: Get.height * 0.19,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
PointsCaptain(
|
||||
kolor: AppColor.greyColor, pricePoint: 100, countPoint: '100'),
|
||||
PointsCaptain(
|
||||
kolor: AppColor.bronze, pricePoint: 200, countPoint: '210'),
|
||||
PointsCaptain(
|
||||
kolor: AppColor.goldenBronze, pricePoint: 400, countPoint: '450'),
|
||||
PointsCaptain(
|
||||
kolor: AppColor.gold, pricePoint: 1000, countPoint: '1100'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ويدجت مُحسّن لعرض صف معلومات الرصيد
|
||||
class _BudgetInfoRow extends StatelessWidget {
|
||||
final String title;
|
||||
final String amount;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _BudgetInfoRow({
|
||||
required this.title,
|
||||
required this.amount,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppStyle.title.copyWith(fontSize: 16),
|
||||
Text("Select how you want to charge your account".tr,
|
||||
style: FinanceDesignSystem.subHeadingStyle),
|
||||
const SizedBox(height: 24),
|
||||
ListTile(
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.accentBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: const Icon(Icons.account_balance_wallet_rounded,
|
||||
color: FinanceDesignSystem.accentBlue),
|
||||
),
|
||||
title: Text("Pay from my budget".tr),
|
||||
subtitle: Text(
|
||||
"${'You have in account'.tr} ${controller.totalAmountVisa}"),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
_showPayFromBudgetDialog(controller);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.accentColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Text(
|
||||
'$amount ${'S.P'.tr}',
|
||||
style:
|
||||
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ويدجت مُحسّن لعرض بطاقة العرض الترويجي
|
||||
class _PromoProgressCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String timePromo;
|
||||
final int count;
|
||||
final int maxCount;
|
||||
final String description;
|
||||
final CaptainWalletController controller;
|
||||
|
||||
const _PromoProgressCard({
|
||||
required this.title,
|
||||
required this.timePromo,
|
||||
required this.count,
|
||||
required this.maxCount,
|
||||
required this.description,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
MyDialog().getDialog(title, description, () async {
|
||||
if (count >= maxCount) {
|
||||
controller.addDriverWalletFromPromo(timePromo, 50);
|
||||
}
|
||||
Get.back();
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
Text("Recharge Balance Packages".tr,
|
||||
style: FinanceDesignSystem.subHeadingStyle
|
||||
.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 140,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: [
|
||||
Text(title,
|
||||
style:
|
||||
AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
'$count / $maxCount',
|
||||
style: AppStyle.title.copyWith(color: AppColor.blueColor),
|
||||
),
|
||||
PointsCaptain(
|
||||
kolor: Colors.blueGrey,
|
||||
pricePoint: 100,
|
||||
countPoint: '100'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.brown, pricePoint: 200, countPoint: '210'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.amber, pricePoint: 400, countPoint: '450'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.orange,
|
||||
pricePoint: 1000,
|
||||
countPoint: '1100'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 12,
|
||||
value: count / maxCount,
|
||||
backgroundColor: AppColor.accentColor.withOpacity(0.2),
|
||||
color: count >= maxCount
|
||||
? AppColor.greenColor
|
||||
: AppColor.blueColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPayFromBudgetDialog(CaptainWalletController controller) {
|
||||
Get.defaultDialog(
|
||||
title: 'Pay from my budget'.tr,
|
||||
content: Form(
|
||||
key: controller.formKey,
|
||||
child: MyTextForm(
|
||||
controller: controller.amountFromBudgetController,
|
||||
label: '${'You have in account'.tr} ${controller.totalAmountVisa}',
|
||||
hint: '${'You have in account'.tr} ${controller.totalAmountVisa}',
|
||||
type: TextInputType.number,
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Pay'.tr,
|
||||
onPressed: () async {
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAvailable) {
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment'.tr,
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
if (double.parse(controller.amountFromBudgetController.text) <
|
||||
double.parse(controller.totalAmountVisa)) {
|
||||
await controller.payFromBudget();
|
||||
} else {
|
||||
Get.back();
|
||||
mySnackeBarError('Your Budget less than needed'.tr);
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'Authentication failed'.tr, ''.tr, () => Get.back());
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr,
|
||||
() => Get.back());
|
||||
}
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(title: 'Cancel'.tr, onPressed: () => Get.back()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- الدوال والويدجتس الخاصة بالدفع في سوريا تبقى كما هي ---
|
||||
// This function is a placeholder for adding Syrian payment methods.
|
||||
// You would implement the UI and logic for mobile wallets or other local options here.
|
||||
Future<dynamic> addSyrianPaymentMethod(
|
||||
CaptainWalletController captainWalletController) {
|
||||
return Get.defaultDialog(
|
||||
@@ -528,15 +377,12 @@ Future<dynamic> addSyrianPaymentMethod(
|
||||
"Insert your mobile wallet details to receive your money weekly"
|
||||
.tr),
|
||||
MyTextForm(
|
||||
controller: captainWalletController
|
||||
.cardBank, // Re-using for mobile number
|
||||
controller: captainWalletController.cardBank,
|
||||
label: "Insert mobile wallet number".tr,
|
||||
hint: '0912 345 678',
|
||||
type: TextInputType.phone),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
MyDropDownSyria() // Dropdown for Syrian providers
|
||||
const SizedBox(height: 10),
|
||||
MyDropDownSyria()
|
||||
],
|
||||
)),
|
||||
confirm: MyElevatedButton(
|
||||
@@ -545,7 +391,6 @@ Future<dynamic> addSyrianPaymentMethod(
|
||||
if (captainWalletController.formKeyAccount.currentState!
|
||||
.validate()) {
|
||||
Get.back();
|
||||
// Replace with your actual API endpoint and payload for Syria
|
||||
var res =
|
||||
await CRUD().post(link: AppLink.updateAccountBank, payload: {
|
||||
"paymentProvider":
|
||||
@@ -554,7 +399,6 @@ Future<dynamic> addSyrianPaymentMethod(
|
||||
captainWalletController.cardBank.text.toString(),
|
||||
"id": box.read(BoxName.driverID).toString()
|
||||
});
|
||||
print('res: $res');
|
||||
if (res != 'failure') {
|
||||
mySnackbarSuccess('Payment details added successfully'.tr);
|
||||
}
|
||||
@@ -562,10 +406,8 @@ Future<dynamic> addSyrianPaymentMethod(
|
||||
}));
|
||||
}
|
||||
|
||||
// A new GetX controller for the Syrian payout dropdown
|
||||
class SyrianPayoutController extends GetxController {
|
||||
String dropdownValue = 'syriatel';
|
||||
|
||||
void changeValue(String? newValue) {
|
||||
if (newValue != null) {
|
||||
dropdownValue = newValue;
|
||||
@@ -574,33 +416,25 @@ class SyrianPayoutController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// A new Dropdown widget for Syrian mobile wallet providers
|
||||
class MyDropDownSyria extends StatelessWidget {
|
||||
const MyDropDownSyria({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(SyrianPayoutController());
|
||||
final theme = Theme.of(context);
|
||||
return GetBuilder<SyrianPayoutController>(builder: (controller) {
|
||||
return DropdownButton<String>(
|
||||
value: controller.dropdownValue,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
elevation: 16,
|
||||
isExpanded: true,
|
||||
style: const TextStyle(color: Colors.deepPurple),
|
||||
underline: Container(
|
||||
height: 2,
|
||||
color: Colors.deepPurpleAccent,
|
||||
),
|
||||
onChanged: (String? newValue) {
|
||||
controller.changeValue(newValue);
|
||||
},
|
||||
dropdownColor: theme.cardColor,
|
||||
style: TextStyle(color: theme.textTheme.bodyLarge?.color),
|
||||
underline: Container(height: 2, color: theme.primaryColor),
|
||||
onChanged: (String? newValue) => controller.changeValue(newValue),
|
||||
items: <String>['syriatel', 'mtn']
|
||||
.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value.tr),
|
||||
);
|
||||
return DropdownMenuItem<String>(value: value, child: Text(value.tr));
|
||||
}).toList(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,11 +2,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
|
||||
import 'package:sefer_driver/constant/colors.dart';
|
||||
import 'package:sefer_driver/constant/style.dart';
|
||||
import 'package:sefer_driver/constant/finance_design_system.dart';
|
||||
import 'package:sefer_driver/views/widgets/mycircular.dart';
|
||||
import '../../../controller/payment/driver_payment_controller.dart';
|
||||
import 'widgets/transaction_preview_item.dart';
|
||||
|
||||
class WeeklyPaymentPage extends StatelessWidget {
|
||||
const WeeklyPaymentPage({super.key});
|
||||
@@ -16,12 +17,18 @@ class WeeklyPaymentPage extends StatelessWidget {
|
||||
Get.put(DriverWalletHistoryController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Weekly Summary'.tr),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 1,
|
||||
title: Text('Weekly Summary'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded, color: FinanceDesignSystem.primaryDark, size: 20),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.grey[100],
|
||||
body: GetBuilder<DriverWalletHistoryController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
@@ -30,45 +37,46 @@ class WeeklyPaymentPage extends StatelessWidget {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// 1. Prominent Summary Card at the top
|
||||
_buildSummaryCard(controller),
|
||||
|
||||
// 2. A title for the transactions list
|
||||
_buildWeeklyStatsHeader(controller),
|
||||
const SizedBox(height: 16),
|
||||
_buildWeekSelector(),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.list_alt, color: Colors.grey[600]),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Transactions this week'.tr,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
Text('Transactions this week'.tr, style: FinanceDesignSystem.headingStyle),
|
||||
const Spacer(),
|
||||
Icon(Icons.list_rounded, color: Colors.grey.shade400, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 3. The animated list of transactions
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: controller.weeklyList.isEmpty
|
||||
? _buildEmptyState(context)
|
||||
: AnimationLimiter(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||
itemCount: controller.weeklyList.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var transaction = controller.weeklyList[index];
|
||||
final tx = controller.weeklyList[index];
|
||||
final double amount = double.tryParse(tx['amount']?.toString() ?? '0') ?? 0;
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
verticalOffset: 25,
|
||||
child: FadeInAnimation(
|
||||
child: _TransactionListItem(
|
||||
transaction: transaction),
|
||||
child: TransactionPreviewItem(
|
||||
title: amount >= 0 ? 'Credit'.tr : 'Debit'.tr,
|
||||
subtitle: tx['dateUpdated'] ?? '',
|
||||
amount: amount.abs().toStringAsFixed(0),
|
||||
date: tx['dateUpdated']?.split(' ')[0] ?? '',
|
||||
type: amount >= 0 ? 'credit' : 'debit',
|
||||
method: tx['paymentMethod'],
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -83,92 +91,87 @@ class WeeklyPaymentPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// A widget for the top summary card.
|
||||
Widget _buildSummaryCard(DriverWalletHistoryController controller) {
|
||||
final String totalAmount = controller.weeklyList.isEmpty
|
||||
Widget _buildWeeklyStatsHeader(DriverWalletHistoryController controller) {
|
||||
final totalAmount = controller.weeklyList.isEmpty
|
||||
? '0.00'
|
||||
: controller.weeklyList[0]['totalAmount']?.toString() ?? '0.00';
|
||||
|
||||
final double earnings = double.tryParse(totalAmount) ?? 0;
|
||||
final int trips = controller.weeklyList.length;
|
||||
final double commission = earnings * 0.15; // Example 15%
|
||||
final double netProfit = earnings - commission;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
elevation: 4,
|
||||
shadowColor: AppColor.primaryColor.withOpacity(0.2),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColor.primaryColor, AppColor.greenColor],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: FinanceDesignSystem.balanceGradient,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: FinanceDesignSystem.primaryDark.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.account_balance_wallet,
|
||||
color: Colors.white, size: 40),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Total Weekly Earnings'.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(color: Colors.white70, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$totalAmount ${'SYP'.tr}',
|
||||
style: AppStyle.number
|
||||
.copyWith(color: Colors.white, fontSize: 32),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text('Total Weekly Earnings'.tr,
|
||||
style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
Text('${earnings.toStringAsFixed(0)} SYP',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 36, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildStatItem('Total Trips'.tr, trips.toString(), Icons.directions_car_rounded),
|
||||
_buildStatItem('Commission'.tr, '${commission.toStringAsFixed(0)}', Icons.percent_rounded),
|
||||
_buildStatItem('Net Profit'.tr, '${netProfit.toStringAsFixed(0)}', Icons.account_balance_rounded),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// A dedicated widget for the list item.
|
||||
Widget _TransactionListItem({required Map<String, dynamic> transaction}) {
|
||||
final String paymentMethod = transaction['paymentMethod'] ?? 'Unknown';
|
||||
final String amount = transaction['amount']?.toString() ?? '0';
|
||||
final DateTime? date = DateTime.tryParse(transaction['dateUpdated'] ?? '');
|
||||
Widget _buildStatItem(String label, String value, IconData icon) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(color: Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(icon, color: Colors.white, size: 18),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(value, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
Text(label, style: TextStyle(color: Colors.white.withOpacity(0.6), fontSize: 10)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withOpacity(0.05),
|
||||
margin: const EdgeInsets.only(bottom: 12.0),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
child: Icon(_getPaymentIcon(paymentMethod),
|
||||
color: AppColor.primaryColor, size: 22),
|
||||
Widget _buildWeekSelector() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 10)],
|
||||
),
|
||||
title: Text(
|
||||
'$amount ${'SYP'.tr}',
|
||||
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text(
|
||||
date != null
|
||||
? DateFormat('EEEE, hh:mm a', Get.locale?.toString())
|
||||
.format(date) // e.g., Tuesday, 10:11 AM
|
||||
: 'Invalid Date',
|
||||
style: AppStyle.title.copyWith(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
trailing: Chip(
|
||||
label: Text(
|
||||
_getTranslatedPaymentMethod(paymentMethod),
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
side: BorderSide.none,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(icon: const Icon(Icons.chevron_left_rounded), onPressed: () {}),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text('Dec 15 - Dec 21, 2024'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: FinanceDesignSystem.primaryDark)),
|
||||
),
|
||||
),
|
||||
IconButton(icon: const Icon(Icons.chevron_right_rounded), onPressed: () {}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -179,45 +182,11 @@ class WeeklyPaymentPage extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.receipt_long_outlined, size: 80, color: Colors.grey[400]),
|
||||
Icon(Icons.bar_chart_rounded, size: 64, color: Colors.grey.shade300),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No transactions this week'.tr,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(color: Colors.grey[600]),
|
||||
),
|
||||
Text('No transactions this week'.tr, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to get a specific icon for each payment method.
|
||||
IconData _getPaymentIcon(String paymentMethod) {
|
||||
switch (paymentMethod.toLowerCase()) {
|
||||
case 'visa':
|
||||
return Icons.credit_card;
|
||||
case 'frombudget':
|
||||
return Icons.account_balance_wallet_outlined;
|
||||
case 'remainder':
|
||||
return Icons.receipt_long;
|
||||
case 'cash':
|
||||
return Icons.money;
|
||||
default:
|
||||
return Icons.payment;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get translated or formatted payment method names.
|
||||
String _getTranslatedPaymentMethod(String paymentMethod) {
|
||||
switch (paymentMethod) {
|
||||
case 'Remainder':
|
||||
return 'Remainder'.tr;
|
||||
case 'fromBudget':
|
||||
return 'From Budget'.tr;
|
||||
default:
|
||||
return paymentMethod;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
124
lib/views/home/my_wallet/widgets/balance_card.dart
Normal file
124
lib/views/home/my_wallet/widgets/balance_card.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class BalanceCard extends StatelessWidget {
|
||||
final String balance;
|
||||
final bool isNegative;
|
||||
final String lastUpdated;
|
||||
|
||||
const BalanceCard({
|
||||
super.key,
|
||||
required this.balance,
|
||||
required this.isNegative,
|
||||
this.lastUpdated = "Just now",
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
gradient: isNegative
|
||||
? FinanceDesignSystem.dangerGradient
|
||||
: FinanceDesignSystem.balanceGradient,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (isNegative
|
||||
? FinanceDesignSystem.dangerRed
|
||||
: FinanceDesignSystem.primaryDark)
|
||||
.withOpacity(0.3),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Available Balance".tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"الرصيد المتاح".tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
isNegative
|
||||
? Icons.warning_rounded
|
||||
: Icons.account_balance_wallet_rounded,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
balance,
|
||||
style: FinanceDesignSystem.balanceStyle,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"SYP".tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Divider(color: Colors.white.withOpacity(0.15)),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.history_rounded,
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"${'Last updated:'.tr} $lastUpdated",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
142
lib/views/home/my_wallet/widgets/financial_summary_card.dart
Normal file
142
lib/views/home/my_wallet/widgets/financial_summary_card.dart
Normal file
@@ -0,0 +1,142 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class FinancialSummaryCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final List<SummaryItem> items;
|
||||
|
||||
const FinancialSummaryCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.items,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: FinanceDesignSystem.headingStyle),
|
||||
if (subtitle != null)
|
||||
Text(subtitle!, style: FinanceDesignSystem.subHeadingStyle),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
Divider(color: Colors.grey.withOpacity(0.1), height: 24),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: item.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(item.icon, color: item.color, size: 20),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
if (item.subLabel != null)
|
||||
Text(
|
||||
item.subLabel!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"${item.amount} ${'SYP'.tr}",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
if (item.trend != null)
|
||||
Text(
|
||||
item.trend!,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: item.trendColor ??
|
||||
FinanceDesignSystem.successGreen,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SummaryItem {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String? subLabel;
|
||||
final String amount;
|
||||
final Color color;
|
||||
final String? trend;
|
||||
final Color? trendColor;
|
||||
|
||||
SummaryItem({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
this.subLabel,
|
||||
required this.amount,
|
||||
required this.color,
|
||||
this.trend,
|
||||
this.trendColor,
|
||||
});
|
||||
}
|
||||
163
lib/views/home/my_wallet/widgets/promo_gamification_card.dart
Normal file
163
lib/views/home/my_wallet/widgets/promo_gamification_card.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class PromoGamificationCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final int currentProgress;
|
||||
final int targetProgress;
|
||||
final String reward;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const PromoGamificationCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.currentProgress,
|
||||
required this.targetProgress,
|
||||
required this.reward,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double progress = (currentProgress / targetProgress).clamp(0.0, 1.0);
|
||||
final bool isCompleted = progress >= 1.0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: (isCompleted
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.accentBlue)
|
||||
.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
reward,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isCompleted
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${'Progress:'.tr} $currentProgress / $targetProgress",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
if (isCompleted)
|
||||
Icon(Icons.check_circle_rounded,
|
||||
color: FinanceDesignSystem.successGreen, size: 16),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 10,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
height: 10,
|
||||
width: MediaQuery.of(context).size.width *
|
||||
progress, // Simplified for now
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: isCompleted
|
||||
? [
|
||||
FinanceDesignSystem.successGreen,
|
||||
Colors.lightGreenAccent
|
||||
]
|
||||
: [FinanceDesignSystem.accentBlue, Colors.blueAccent],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isCompleted && onTap != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: FinanceDesignSystem.successGreen,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(FinanceDesignSystem.buttonRadius),
|
||||
),
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text("Claim Reward".tr),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
120
lib/views/home/my_wallet/widgets/quick_actions.dart
Normal file
120
lib/views/home/my_wallet/widgets/quick_actions.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class QuickActionsGrid extends StatelessWidget {
|
||||
final VoidCallback onAddBalance;
|
||||
final VoidCallback onWithdraw;
|
||||
final VoidCallback onTransfer;
|
||||
final VoidCallback onHistory;
|
||||
|
||||
const QuickActionsGrid({
|
||||
super.key,
|
||||
required this.onAddBalance,
|
||||
required this.onWithdraw,
|
||||
required this.onTransfer,
|
||||
required this.onHistory,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 1.5,
|
||||
children: [
|
||||
_ActionItem(
|
||||
icon: Icons.add_circle_outline_rounded,
|
||||
label: "Add Balance".tr,
|
||||
subLabel: "شحن",
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
onTap: onAddBalance,
|
||||
),
|
||||
_ActionItem(
|
||||
icon: Icons.file_download_outlined,
|
||||
label: "Withdraw".tr,
|
||||
subLabel: "سحب",
|
||||
color: FinanceDesignSystem.successGreen,
|
||||
onTap: onWithdraw,
|
||||
),
|
||||
_ActionItem(
|
||||
icon: Icons.swap_horiz_rounded,
|
||||
label: "Transfer".tr,
|
||||
subLabel: "تحويل",
|
||||
color: Colors.orange,
|
||||
onTap: onTransfer,
|
||||
),
|
||||
_ActionItem(
|
||||
icon: Icons.history_rounded,
|
||||
label: "History".tr,
|
||||
subLabel: "السجل",
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
onTap: onHistory,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String subLabel;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ActionItem({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.subLabel,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.buttonRadius),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.buttonRadius),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
borderRadius:
|
||||
BorderRadius.circular(FinanceDesignSystem.buttonRadius),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 24),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
subLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
128
lib/views/home/my_wallet/widgets/transaction_preview_item.dart
Normal file
128
lib/views/home/my_wallet/widgets/transaction_preview_item.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class TransactionPreviewItem extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String amount;
|
||||
final String date;
|
||||
final String type; // 'credit' or 'debit'
|
||||
final String? method;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const TransactionPreviewItem({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.amount,
|
||||
required this.date,
|
||||
required this.type,
|
||||
this.method,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isCredit = type.toLowerCase() == 'credit';
|
||||
final Color statusColor = isCredit
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.dangerRed;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.mainRadius),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
isCredit
|
||||
? Icons.arrow_downward_rounded
|
||||
: Icons.arrow_upward_rounded,
|
||||
color: statusColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
date,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
if (method != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 3,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade400,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
method!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"${isCredit ? '+' : '-'}$amount ${'SYP'.tr}",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/home/captin/behavior_controller.dart';
|
||||
|
||||
class BehaviorPage extends StatelessWidget {
|
||||
@@ -12,6 +13,7 @@ class BehaviorPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(DriverBehaviorController());
|
||||
controller.fetchDriverBehavior();
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -37,20 +39,23 @@ class BehaviorPage extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
Text("Overall Behavior Score".tr,
|
||||
style: TextStyle(
|
||||
fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
style: theme.textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"${controller.overallScore.value.toStringAsFixed(1)} / 100",
|
||||
style: const TextStyle(
|
||||
fontSize: 28, color: Colors.blue)),
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.primaryColor)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text("Last 10 Trips".tr,
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
@@ -62,7 +67,7 @@ class BehaviorPage extends StatelessWidget {
|
||||
elevation: 3,
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.blue,
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
child: Text("${index + 1}",
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
|
||||
@@ -41,50 +41,91 @@ class CaptainsCars extends StatelessWidget {
|
||||
itemBuilder: (context, index) {
|
||||
final car = controller.cars[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
|
||||
child: Card(
|
||||
color: car['isDefault'].toString() == '0'
|
||||
? AppColor.accentColor
|
||||
: AppColor.blueColor,
|
||||
elevation: 2,
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? AppColor.primaryColor
|
||||
: Theme.of(context).cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: car['isDefault'].toString() == '1'
|
||||
? BorderSide.none
|
||||
: BorderSide(color: Theme.of(context).dividerColor)),
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
Fontisto.car,
|
||||
size: 50,
|
||||
color: Color(int.parse(car['color_hex']
|
||||
.replaceFirst('#', '0xff'))),
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white12,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Fontisto.car,
|
||||
size: 32,
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white
|
||||
: Color(int.parse(car['color_hex']
|
||||
.replaceFirst('#', '0xff'))),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
car['make'],
|
||||
style: AppStyle.title,
|
||||
), // Assuming `make` is a field in each car item
|
||||
subtitle: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
car['model'],
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: AppColor.blueColor)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4),
|
||||
child: Text(
|
||||
(car['car_plate']),
|
||||
style: AppStyle.title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white
|
||||
: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
car['model'],
|
||||
style: TextStyle(
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white70
|
||||
: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
car['year'],
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
), // Assuming `model` is a field in each car item
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white54
|
||||
: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
car['car_plate'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white
|
||||
: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
car['year'],
|
||||
style: TextStyle(
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white70
|
||||
: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// trailing: IconButton(
|
||||
// icon: const Icon(
|
||||
// Icons.delete,
|
||||
|
||||
@@ -99,29 +99,30 @@ class ProfileHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundColor: Get.theme.primaryColor.withOpacity(0.1),
|
||||
child: Icon(Icons.person, size: 60, color: Get.theme.primaryColor),
|
||||
backgroundColor: theme.primaryColor.withOpacity(0.1),
|
||||
child: Icon(Icons.person, size: 60, color: theme.primaryColor),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
name,
|
||||
style: Get.textTheme.headlineSmall
|
||||
style: theme.textTheme.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.star, color: Colors.amber, size: 20),
|
||||
const Icon(Icons.star, color: Colors.amber, size: 20),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${rating.toStringAsFixed(1)} (${'reviews'.tr} $ratingCount)',
|
||||
style: Get.textTheme.titleMedium
|
||||
?.copyWith(color: Colors.grey.shade600),
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(color: theme.hintColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -130,6 +131,7 @@ class ProfileHeader extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 2. ويدجت شبكة الأزرار
|
||||
class ActionsGrid extends StatelessWidget {
|
||||
const ActionsGrid({super.key});
|
||||
@@ -187,20 +189,23 @@ void showShamCashInput() {
|
||||
TextEditingController(text: existingCode);
|
||||
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(25),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black26, blurRadius: 10, offset: Offset(0, -2))
|
||||
],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
Builder(builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(25),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.shadowColor.withOpacity(0.2), blurRadius: 10, offset: const Offset(0, -2))
|
||||
],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
||||
children: [
|
||||
// --- 1. المقبض العلوي ---
|
||||
Center(
|
||||
@@ -208,12 +213,13 @@ void showShamCashInput() {
|
||||
height: 5,
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
color: theme.dividerColor,
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// --- 2. العنوان والأيقونة ---
|
||||
Image.asset(
|
||||
'assets/images/shamCash.png',
|
||||
@@ -386,11 +392,13 @@ void showShamCashInput() {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
isScrollControlled: true, // ضروري لرفع النافذة عند فتح الكيبورد
|
||||
);
|
||||
}),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// ويدجت داخلية لزر في الشبكة
|
||||
class _ActionTile extends StatelessWidget {
|
||||
final String title;
|
||||
@@ -402,8 +410,9 @@ class _ActionTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Material(
|
||||
color: Colors.grey.shade100,
|
||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
@@ -413,12 +422,12 @@ class _ActionTile extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: Get.theme.primaryColor, size: 20),
|
||||
Icon(icon, color: theme.primaryColor, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
style: Get.textTheme.labelLarge,
|
||||
style: theme.textTheme.labelLarge,
|
||||
textAlign: TextAlign.center,
|
||||
)),
|
||||
],
|
||||
@@ -429,6 +438,7 @@ class _ActionTile extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 3. بطاقة المعلومات الشخصية
|
||||
class PersonalInfoCard extends StatelessWidget {
|
||||
final Map<String, dynamic> data;
|
||||
@@ -551,19 +561,21 @@ class _InfoRow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.grey.shade500, size: 20),
|
||||
Icon(icon, color: theme.hintColor.withOpacity(0.6), size: 20),
|
||||
const SizedBox(width: 16),
|
||||
Text(label, style: Get.textTheme.bodyLarge),
|
||||
Text(label, style: theme.textTheme.bodyLarge),
|
||||
const Spacer(),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: Get.textTheme.bodyLarge?.copyWith(
|
||||
color: Colors.grey.shade700, fontWeight: FontWeight.w500),
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.textTheme.bodyLarge?.color?.withOpacity(0.8),
|
||||
fontWeight: FontWeight.w500),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
@@ -572,3 +584,4 @@ class _InfoRow extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,77 +26,91 @@ class PromosPassengerPage extends StatelessWidget {
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final rides = orderHistoryController.promoList[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
color: AppColor.secondaryColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.accentColor,
|
||||
offset: Offset(-3, -3),
|
||||
blurRadius: 0,
|
||||
spreadRadius: 0,
|
||||
blurStyle: BlurStyle.outer),
|
||||
BoxShadow(
|
||||
color: AppColor.accentColor,
|
||||
offset: Offset(3, 3),
|
||||
blurRadius: 0,
|
||||
spreadRadius: 0,
|
||||
blurStyle: BlurStyle.outer)
|
||||
]),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shadowColor:
|
||||
Theme.of(context).shadowColor.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
AnimatedTextKit(
|
||||
animatedTexts: [
|
||||
ScaleAnimatedText(rides['promo_code'],
|
||||
textStyle: AppStyle.title),
|
||||
WavyAnimatedText(rides['promo_code'],
|
||||
textStyle: AppStyle.title),
|
||||
FlickerAnimatedText(
|
||||
rides['promo_code'],
|
||||
textStyle: AppStyle.title),
|
||||
WavyAnimatedText(rides['promo_code'],
|
||||
textStyle: AppStyle.title),
|
||||
],
|
||||
isRepeatingAnimation: true,
|
||||
onTap: () {},
|
||||
),
|
||||
Text(
|
||||
rides['description'],
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: AnimatedTextKit(
|
||||
animatedTexts: [
|
||||
ScaleAnimatedText(
|
||||
rides['promo_code'],
|
||||
textStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(
|
||||
color: AppColor
|
||||
.primaryColor,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
)),
|
||||
WavyAnimatedText(
|
||||
rides['promo_code'],
|
||||
textStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(
|
||||
color: AppColor
|
||||
.primaryColor,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
)),
|
||||
],
|
||||
repeatForever: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
rides['description'],
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
rides['validity_start_date'],
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
rides['validity_end_date'],
|
||||
style: AppStyle.title,
|
||||
),
|
||||
_buildDateBadge(context,
|
||||
rides['validity_start_date'], true),
|
||||
const SizedBox(height: 4),
|
||||
_buildDateBadge(context,
|
||||
rides['validity_end_date'], false),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
'Copy this Promo to use it in your Ride!'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.headTitle2
|
||||
.copyWith(color: AppColor.accentColor),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium
|
||||
?.copyWith(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -109,4 +123,24 @@ class PromosPassengerPage extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateBadge(BuildContext context, String date, bool isStart) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isStart
|
||||
? Colors.green.withOpacity(0.1)
|
||||
: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
date,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isStart ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/constant/style.dart';
|
||||
import 'package:sefer_driver/views/widgets/my_scafold.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
|
||||
class TaarifPage extends StatelessWidget {
|
||||
const TaarifPage({super.key});
|
||||
|
||||
@@ -18,47 +20,35 @@ class TaarifPage extends StatelessWidget {
|
||||
// crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
Table(
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
border: TableBorder.symmetric(),
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
TableRow(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
children: [
|
||||
Text('Minimum fare'.tr, style: AppStyle.title),
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? Text('1 ${'JOD'.tr}', style: AppStyle.title)
|
||||
: Text('20 ${'LE'.tr}', style: AppStyle.title),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
Text('Maximum fare'.tr, style: AppStyle.title),
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? Text('200 ${'JOD'.tr}', style: AppStyle.title)
|
||||
: Text('15000 ${'LE'.tr}', style: AppStyle.title),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
Text('Flag-down fee'.tr, style: AppStyle.title),
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? Text('0.47 ${'JOD'.tr}', style: AppStyle.title)
|
||||
: Text('15 ${'LE'.tr}', style: AppStyle.title),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? Text('0.05 ${'JOD'.tr}/min and 0.21 ${'JOD'.tr}/km',
|
||||
style: AppStyle.title)
|
||||
: Text('1 ${'LE'.tr}/min and 4 ${'LE'.tr}/km',
|
||||
style: AppStyle.title),
|
||||
Text('Including Tax'.tr, style: AppStyle.title),
|
||||
],
|
||||
),
|
||||
],
|
||||
_buildTariffItem(
|
||||
context,
|
||||
'Minimum fare'.tr,
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? '1 ${'JOD'.tr}'
|
||||
: '20 ${'LE'.tr}'),
|
||||
_buildTariffItem(
|
||||
context,
|
||||
'Maximum fare'.tr,
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? '200 ${'JOD'.tr}'
|
||||
: '15000 ${'LE'.tr}'),
|
||||
_buildTariffItem(
|
||||
context,
|
||||
'Flag-down fee'.tr,
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? '0.47 ${'JOD'.tr}'
|
||||
: '15 ${'LE'.tr}'),
|
||||
_buildTariffItem(
|
||||
context,
|
||||
'Rate'.tr,
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? '0.05 ${'JOD'.tr}/min and 0.21 ${'JOD'.tr}/km'
|
||||
: '1 ${'LE'.tr}/min and 4 ${'LE'.tr}/km'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text('Including Tax'.tr,
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(color: Theme.of(context).hintColor)),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text('BookingFee'.tr, style: AppStyle.headTitle2),
|
||||
@@ -85,4 +75,21 @@ class TaarifPage extends StatelessWidget {
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildTariffItem(BuildContext context, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(label, style: AppStyle.title),
|
||||
),
|
||||
Text(value,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.bold, color: AppColor.primaryColor)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||
|
||||
import '../../constant/finance_design_system.dart';
|
||||
import '../../controller/local/local_controller.dart';
|
||||
|
||||
class Language extends StatelessWidget {
|
||||
@@ -32,33 +33,9 @@ class Language extends StatelessWidget {
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: [
|
||||
_buildLanguageButton(
|
||||
'العربية', 'ar', controller, context, '🇪🇬'),
|
||||
_buildLanguageButton('العربية (الخليج)', 'ar-gulf',
|
||||
controller, context, '🇸🇦'),
|
||||
_buildLanguageButton('العربية (المغرب)', 'ar-ma',
|
||||
controller, context, '🇲🇦'),
|
||||
'العربية', 'ar', controller, context, 'AR'),
|
||||
_buildLanguageButton(
|
||||
'English', 'en', controller, context, '🇺🇸'),
|
||||
_buildLanguageButton(
|
||||
'Türkçe', 'tr', controller, context, '🇹🇷'),
|
||||
_buildLanguageButton(
|
||||
'Français', 'fr', controller, context, '🇫🇷'),
|
||||
_buildLanguageButton(
|
||||
'Italiano', 'it', controller, context, '🇮🇹'),
|
||||
_buildLanguageButton(
|
||||
'Deutsch', 'de', controller, context, '🇩🇪'),
|
||||
_buildLanguageButton(
|
||||
'Ελληνικά', 'el', controller, context, '🇬🇷'),
|
||||
_buildLanguageButton(
|
||||
'Español', 'es', controller, context, '🇪🇸'),
|
||||
_buildLanguageButton(
|
||||
'فارسی', 'fa', controller, context, '🇮🇷'),
|
||||
_buildLanguageButton(
|
||||
'中文', 'zh', controller, context, '🇨🇳'),
|
||||
_buildLanguageButton(
|
||||
'Русский', 'ru', controller, context, '🇷🇺'),
|
||||
_buildLanguageButton(
|
||||
'हिन्दी', 'hi', controller, context, '🇮🇳'),
|
||||
'English', 'en', controller, context, 'EN'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -104,25 +81,43 @@ class Language extends StatelessWidget {
|
||||
Widget _buildLanguageButton(String title, String langCode,
|
||||
LocaleController controller, BuildContext context, String flagIcon) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: CupertinoColors.systemGrey5.withOpacity(0.5),
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Text(flagIcon,
|
||||
style: const TextStyle(fontSize: 28)), // Using flag icon as leading
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
leading: Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.primaryDark.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
flagIcon,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(CupertinoIcons.chevron_forward,
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:convert';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart'; // لتحديد الأنواع إذا لزم
|
||||
import 'package:intaleq_maps/intaleq_maps.dart'; // لتحديد الأنواع إذا لزم
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
@@ -114,7 +114,7 @@ class RideAvailableCard extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
shadowColor: Theme.of(context).shadowColor.withOpacity(0.1),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
@@ -179,7 +179,7 @@ class RideAvailableCard extends StatelessWidget {
|
||||
Container(
|
||||
height: 30,
|
||||
width: 1,
|
||||
color: Colors.grey.shade300,
|
||||
color: Theme.of(Get.context!).dividerColor,
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
),
|
||||
const Icon(Icons.location_on, color: Colors.red, size: 18),
|
||||
@@ -218,15 +218,17 @@ class RideAvailableCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoItem(IconData icon, String text,
|
||||
{Color iconColor = Colors.grey}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: iconColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(text, style: AppStyle.subtitle.copyWith(fontSize: 13)),
|
||||
],
|
||||
);
|
||||
Widget _infoItem(IconData icon, String text, {Color? iconColor}) {
|
||||
return Builder(builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: iconColor ?? theme.hintColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(text, style: theme.textTheme.bodySmall?.copyWith(fontSize: 13)),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -6,65 +6,72 @@ import 'mydialoug.dart';
|
||||
|
||||
class MyCircleContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final Color backgroundColor;
|
||||
final Color borderColor;
|
||||
final Color? backgroundColor;
|
||||
final Color? borderColor;
|
||||
|
||||
MyCircleContainer({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.child,
|
||||
this.backgroundColor = AppColor.secondaryColor,
|
||||
this.borderColor = AppColor.accentColor,
|
||||
}) : super(key: key);
|
||||
this.backgroundColor,
|
||||
this.borderColor,
|
||||
});
|
||||
|
||||
final controller = Get.put(CircleController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveBorderColor = borderColor ?? AppColor.borderColor;
|
||||
|
||||
return GetBuilder<CircleController>(
|
||||
builder: ((controller) => GestureDetector(
|
||||
onTap: () {
|
||||
controller.changeColor();
|
||||
MyDialog().getDialog(
|
||||
'Rejected Orders Count'.tr,
|
||||
'This is the total number of rejected orders per day after accepting the orders'
|
||||
.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
onEnd: () {
|
||||
controller.onEnd();
|
||||
builder: ((controller) {
|
||||
final effectiveBackgroundColor = backgroundColor ??
|
||||
(controller.isActive ? AppColor.accentColor : AppColor.secondaryColor);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
controller.toggleActive();
|
||||
MyDialog().getDialog(
|
||||
'Rejected Orders Count'.tr,
|
||||
'This is the total number of rejected orders per day after accepting the orders'
|
||||
.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
},
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: controller.size,
|
||||
height: controller.size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: controller.backgroundColor,
|
||||
border: Border.all(
|
||||
color: borderColor,
|
||||
width: 1,
|
||||
child: AnimatedContainer(
|
||||
onEnd: () {
|
||||
controller.resetSize();
|
||||
},
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: controller.size,
|
||||
height: controller.size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: effectiveBackgroundColor,
|
||||
border: Border.all(
|
||||
color: effectiveBorderColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Center(child: child),
|
||||
),
|
||||
child: Center(child: child),
|
||||
),
|
||||
)));
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
class CircleController extends GetxController {
|
||||
Color backgroundColor = AppColor.secondaryColor;
|
||||
bool isActive = false;
|
||||
double size = 40;
|
||||
void changeColor() {
|
||||
backgroundColor = backgroundColor == AppColor.secondaryColor
|
||||
? AppColor.accentColor
|
||||
: AppColor.secondaryColor;
|
||||
|
||||
void toggleActive() {
|
||||
isActive = !isActive;
|
||||
size = 60;
|
||||
update();
|
||||
}
|
||||
|
||||
void onEnd() {
|
||||
void resetSize() {
|
||||
size = 40;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,8 +51,14 @@ class MyElevatedButton extends StatelessWidget {
|
||||
child: Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title.copyWith(color: AppColor.secondaryColor),
|
||||
style: AppStyle.title.copyWith(
|
||||
color: (kolor == AppColor.primaryColor || kolor == AppColor.blueColor || kolor == AppColor.redColor || kolor == AppColor.greenColor)
|
||||
? Colors.white
|
||||
: AppColor.writeColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,123 +1,292 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
|
||||
class SnackbarConfig {
|
||||
static const duration = Duration(seconds: 3);
|
||||
static const animationDuration = Duration(milliseconds: 300);
|
||||
static const margin = EdgeInsets.symmetric(horizontal: 16, vertical: 10);
|
||||
static const borderRadius = 12.0;
|
||||
static const elevation = 6.0;
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Snackbar variant definition
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
enum _SnackVariant { success, error, info, warning }
|
||||
|
||||
static final BoxShadow shadow = BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
);
|
||||
extension _VariantProps on _SnackVariant {
|
||||
Color get baseColor => switch (this) {
|
||||
_SnackVariant.success => const Color(0xFF1A9E5C),
|
||||
_SnackVariant.error => const Color(0xFFD93025),
|
||||
_SnackVariant.info => const Color(0xFF1A73E8),
|
||||
_SnackVariant.warning => const Color(0xFFF29900),
|
||||
};
|
||||
|
||||
Color get surfaceColor => switch (this) {
|
||||
_SnackVariant.success => const Color(0xFFF0FBF5),
|
||||
_SnackVariant.error => const Color(0xFFFEF2F1),
|
||||
_SnackVariant.info => const Color(0xFFF0F6FF),
|
||||
_SnackVariant.warning => const Color(0xFFFFF8E6),
|
||||
};
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
_SnackVariant.success => Icons.check_circle_rounded,
|
||||
_SnackVariant.error => Icons.error_rounded,
|
||||
_SnackVariant.info => Icons.info_rounded,
|
||||
_SnackVariant.warning => Icons.warning_amber_rounded,
|
||||
};
|
||||
|
||||
String get label => switch (this) {
|
||||
_SnackVariant.success => 'Success',
|
||||
_SnackVariant.error => 'Error',
|
||||
_SnackVariant.info => 'Info',
|
||||
_SnackVariant.warning => 'Warning',
|
||||
};
|
||||
|
||||
HapticFeedbackType get haptic => switch (this) {
|
||||
_SnackVariant.error => HapticFeedbackType.medium,
|
||||
_SnackVariant.warning => HapticFeedbackType.medium,
|
||||
_SnackVariant.success => HapticFeedbackType.light,
|
||||
_SnackVariant.info => HapticFeedbackType.selection,
|
||||
};
|
||||
}
|
||||
|
||||
SnackbarController mySnackeBarError(String message) {
|
||||
// Trigger error haptic feedback
|
||||
HapticFeedback.mediumImpact();
|
||||
enum HapticFeedbackType { light, medium, selection }
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Core snackbar widget
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
class _SnackContent extends StatefulWidget {
|
||||
final String message;
|
||||
final _SnackVariant variant;
|
||||
|
||||
const _SnackContent({required this.message, required this.variant});
|
||||
|
||||
@override
|
||||
State<_SnackContent> createState() => _SnackContentState();
|
||||
}
|
||||
|
||||
class _SnackContentState extends State<_SnackContent>
|
||||
with TickerProviderStateMixin {
|
||||
late final AnimationController _ctrl;
|
||||
late final Animation<double> _scaleAnim;
|
||||
late final Animation<double> _progressAnim;
|
||||
|
||||
static const Duration _displayDuration = Duration(seconds: 4);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ctrl = AnimationController(vsync: this, duration: _displayDuration);
|
||||
|
||||
_scaleAnim = CurvedAnimation(
|
||||
parent: AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
)..forward(),
|
||||
curve: Curves.elasticOut,
|
||||
);
|
||||
|
||||
_progressAnim = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||
CurvedAnimation(parent: _ctrl, curve: Curves.linear),
|
||||
);
|
||||
|
||||
_ctrl.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final v = widget.variant;
|
||||
final accent = v.baseColor;
|
||||
final surface = v.surfaceColor;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: surface,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: accent.withOpacity(0.18), width: 1.2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: accent.withOpacity(0.12),
|
||||
blurRadius: 20,
|
||||
spreadRadius: -2,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.06),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// ── Main row ──────────────────────────────────────────────────
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 10, 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Animated icon badge
|
||||
ScaleTransition(
|
||||
scale: _scaleAnim,
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: accent.withOpacity(0.12),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(v.icon, color: accent, size: 22),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Text content
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
v.label.tr,
|
||||
style: TextStyle(
|
||||
color: accent,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
widget.message,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[800],
|
||||
fontSize: 13.5,
|
||||
height: 1.4,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Close button
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
Get.closeCurrentSnackbar();
|
||||
},
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close_rounded,
|
||||
size: 16,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── Thin progress strip ───────────────────────────────────────
|
||||
AnimatedBuilder(
|
||||
animation: _progressAnim,
|
||||
builder: (_, __) => Stack(
|
||||
children: [
|
||||
// Track
|
||||
Container(
|
||||
height: 3,
|
||||
color: accent.withOpacity(0.08),
|
||||
),
|
||||
// Fill
|
||||
FractionallySizedBox(
|
||||
widthFactor: _progressAnim.value,
|
||||
child: Container(
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: accent.withOpacity(0.45),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(4),
|
||||
bottomRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Internal dispatcher — single source of truth
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
SnackbarController _show(_SnackVariant variant, String message) {
|
||||
// Dismiss any existing snackbar first (no stacking)
|
||||
if (Get.isSnackbarOpen) Get.closeCurrentSnackbar();
|
||||
|
||||
switch (variant.haptic) {
|
||||
case HapticFeedbackType.light:
|
||||
HapticFeedback.lightImpact();
|
||||
case HapticFeedbackType.medium:
|
||||
HapticFeedback.mediumImpact();
|
||||
case HapticFeedbackType.selection:
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
|
||||
return Get.snackbar(
|
||||
'Error'.tr,
|
||||
message,
|
||||
backgroundColor: AppColor.redColor.withOpacity(0.95),
|
||||
colorText: AppColor.secondaryColor,
|
||||
icon: const Icon(
|
||||
Icons.error_outline_rounded,
|
||||
color: AppColor.secondaryColor,
|
||||
size: 28,
|
||||
),
|
||||
shouldIconPulse: true,
|
||||
'',
|
||||
'',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
margin: SnackbarConfig.margin,
|
||||
borderRadius: SnackbarConfig.borderRadius,
|
||||
duration: SnackbarConfig.duration,
|
||||
animationDuration: SnackbarConfig.animationDuration,
|
||||
forwardAnimationCurve: Curves.easeOutCirc,
|
||||
reverseAnimationCurve: Curves.easeInCirc,
|
||||
boxShadows: [SnackbarConfig.shadow],
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
titleText: Text(
|
||||
'Error'.tr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
messageText: Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
fontSize: 14,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
onTap: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
Get.closeCurrentSnackbar();
|
||||
},
|
||||
backgroundColor: Colors.transparent,
|
||||
margin: EdgeInsets.zero,
|
||||
padding: EdgeInsets.zero,
|
||||
duration: const Duration(seconds: 4),
|
||||
animationDuration: const Duration(milliseconds: 380),
|
||||
barBlur: 0,
|
||||
overlayBlur: 0,
|
||||
overlayColor: Colors.transparent,
|
||||
isDismissible: true,
|
||||
dismissDirection: DismissDirection.horizontal,
|
||||
overlayBlur: 0.8,
|
||||
overlayColor: Colors.black12,
|
||||
dismissDirection: DismissDirection.up,
|
||||
forwardAnimationCurve: Curves.easeOutCubic,
|
||||
reverseAnimationCurve: Curves.easeInCubic,
|
||||
snackStyle: SnackStyle.FLOATING,
|
||||
userInputForm: Form(
|
||||
child: _SnackContent(message: message, variant: variant),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SnackbarController mySnackbarSuccess(String message) {
|
||||
// Trigger success haptic feedback
|
||||
HapticFeedback.lightImpact();
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Public API — drop-in replacements for the old functions
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
SnackbarController mySnackbarSuccess(String message) =>
|
||||
_show(_SnackVariant.success, message);
|
||||
|
||||
return Get.snackbar(
|
||||
'Success'.tr,
|
||||
message,
|
||||
backgroundColor: AppColor.greenColor.withOpacity(0.95),
|
||||
colorText: AppColor.secondaryColor,
|
||||
icon: const Icon(
|
||||
Icons.check_circle_outline_rounded,
|
||||
color: AppColor.secondaryColor,
|
||||
size: 28,
|
||||
),
|
||||
shouldIconPulse: true,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
margin: SnackbarConfig.margin,
|
||||
borderRadius: SnackbarConfig.borderRadius,
|
||||
duration: SnackbarConfig.duration,
|
||||
animationDuration: SnackbarConfig.animationDuration,
|
||||
forwardAnimationCurve: Curves.easeOutCirc,
|
||||
reverseAnimationCurve: Curves.easeInCirc,
|
||||
boxShadows: [SnackbarConfig.shadow],
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
titleText: Text(
|
||||
'Success'.tr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
messageText: Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
fontSize: 14,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
onTap: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
Get.closeCurrentSnackbar();
|
||||
},
|
||||
isDismissible: true,
|
||||
dismissDirection: DismissDirection.horizontal,
|
||||
overlayBlur: 0.8,
|
||||
overlayColor: Colors.black12,
|
||||
);
|
||||
}
|
||||
SnackbarController mySnackeBarError(String message) =>
|
||||
_show(_SnackVariant.error, message);
|
||||
|
||||
SnackbarController mySnackbarInfo(String message) =>
|
||||
_show(_SnackVariant.info, message);
|
||||
|
||||
SnackbarController mySnackbarWarning(String message) =>
|
||||
_show(_SnackVariant.warning, message);
|
||||
|
||||
@@ -26,18 +26,18 @@ class IconWidgetMenu extends StatelessWidget {
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
decoration: const BoxDecoration(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.secondaryColor,
|
||||
offset: Offset(-2, -2),
|
||||
offset: const Offset(-2, -2),
|
||||
blurRadius: 0,
|
||||
spreadRadius: 0,
|
||||
blurStyle: BlurStyle.outer,
|
||||
),
|
||||
BoxShadow(
|
||||
const BoxShadow(
|
||||
color: AppColor.accentColor,
|
||||
offset: Offset(3, 3),
|
||||
blurRadius: 0,
|
||||
|
||||
@@ -9,29 +9,27 @@ class MyScafolld extends StatelessWidget {
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.body,
|
||||
this.action = const Icon(
|
||||
Icons.clear,
|
||||
color: AppColor.secondaryColor,
|
||||
),
|
||||
this.action,
|
||||
required this.isleading,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final List<Widget> body;
|
||||
final Widget action;
|
||||
final Widget? action;
|
||||
final bool isleading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.secondaryColor,
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColor.secondaryColor,
|
||||
backgroundColor: theme.appBarTheme.backgroundColor,
|
||||
elevation: 0,
|
||||
leading: isleading
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Navigator.maybePop(context);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios_new,
|
||||
@@ -39,12 +37,19 @@ class MyScafolld extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
actions: [action],
|
||||
actions: [
|
||||
action ??
|
||||
Icon(
|
||||
Icons.clear,
|
||||
color: Colors.transparent,
|
||||
)
|
||||
],
|
||||
title: Text(
|
||||
title,
|
||||
style: AppStyle.title.copyWith(fontSize: 30),
|
||||
style: theme.textTheme.titleLarge?.copyWith(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
body: SafeArea(child: Stack(children: body)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
@@ -18,6 +19,7 @@ class MyTextForm extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: SizedBox(
|
||||
@@ -27,9 +29,7 @@ class MyTextForm extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
label.tr,
|
||||
style: TextStyle(
|
||||
color: CupertinoColors.label,
|
||||
fontSize: 16,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@@ -40,13 +40,14 @@ class MyTextForm extends StatelessWidget {
|
||||
placeholder: hint.tr,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.systemBackground,
|
||||
border: Border.all(color: CupertinoColors.systemGrey4),
|
||||
color: theme.cardColor,
|
||||
border: Border.all(color: theme.dividerColor),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
style: const TextStyle(color: CupertinoColors.label),
|
||||
placeholderStyle:
|
||||
const TextStyle(color: CupertinoColors.placeholderText),
|
||||
style: theme.textTheme.bodyLarge,
|
||||
placeholderStyle: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.hintColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ValueListenableBuilder<TextEditingValue>(
|
||||
@@ -56,9 +57,10 @@ class MyTextForm extends StatelessWidget {
|
||||
return errorText != null
|
||||
? Text(
|
||||
errorText,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.destructiveRed,
|
||||
fontSize: 12),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.error,
|
||||
fontSize: 12,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
|
||||
@@ -44,11 +44,9 @@ class MyDialog extends GetxController {
|
||||
sigmaX: DialogConfig.blurStrength,
|
||||
sigmaY: DialogConfig.blurStrength,
|
||||
),
|
||||
child: Theme(
|
||||
data: ThemeData.light().copyWith(
|
||||
dialogBackgroundColor: CupertinoColors.systemBackground,
|
||||
),
|
||||
child: CupertinoAlertDialog(
|
||||
child: Builder(builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
return CupertinoAlertDialog(
|
||||
title: Column(
|
||||
children: [
|
||||
Text(
|
||||
@@ -92,7 +90,7 @@ class MyDialog extends GetxController {
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 16,
|
||||
height: 1.3,
|
||||
color: Colors.black87,
|
||||
color: theme.textTheme.bodyLarge?.color ?? AppColor.writeColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -102,7 +100,7 @@ class MyDialog extends GetxController {
|
||||
CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
Get.back();
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
},
|
||||
child: Text(
|
||||
'Cancel'.tr,
|
||||
@@ -116,6 +114,7 @@ class MyDialog extends GetxController {
|
||||
CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
onPressed();
|
||||
},
|
||||
child: Text(
|
||||
@@ -128,10 +127,11 @@ class MyDialog extends GetxController {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
barrierDismissible: true,
|
||||
barrierColor: Colors.black.withAlpha(102), // 0.4 opacity
|
||||
);
|
||||
@@ -159,11 +159,8 @@ class MyDialogContent extends GetxController {
|
||||
sigmaX: DialogConfig.blurStrength,
|
||||
sigmaY: DialogConfig.blurStrength,
|
||||
),
|
||||
child: Theme(
|
||||
data: ThemeData.light().copyWith(
|
||||
dialogBackgroundColor: CupertinoColors.systemBackground,
|
||||
),
|
||||
child: CupertinoAlertDialog(
|
||||
child: Builder(builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Column(
|
||||
children: [
|
||||
Text(
|
||||
@@ -208,7 +205,7 @@ class MyDialogContent extends GetxController {
|
||||
CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
Get.back();
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
},
|
||||
child: Text(
|
||||
'Cancel',
|
||||
@@ -222,6 +219,7 @@ class MyDialogContent extends GetxController {
|
||||
CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
onPressed();
|
||||
},
|
||||
child: Text(
|
||||
@@ -234,10 +232,11 @@ class MyDialogContent extends GetxController {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
barrierDismissible: true,
|
||||
barrierColor: Colors.black.withAlpha(102), // 0.4 opacity
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user