first commit
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/links.dart';
|
||||
import '../../views/widgets/elevated_btn.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class BlinkingController extends GetxController {
|
||||
final promoFormKey = GlobalKey<FormState>();
|
||||
|
||||
final promo = TextEditingController();
|
||||
bool promoTaken = false;
|
||||
void applyPromoCodeToPassenger() async {
|
||||
//TAWJIHI
|
||||
if (promoFormKey.currentState!.validate()) {
|
||||
CRUD().get(link: AppLink.getPassengersPromo, payload: {
|
||||
'promo_code': promo.text,
|
||||
}).then((value) {
|
||||
if (value == 'failure') {
|
||||
Get.defaultDialog(
|
||||
title: 'Promo End !'.tr,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Back',
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.back();
|
||||
},
|
||||
));
|
||||
}
|
||||
var decode = jsonDecode(value);
|
||||
|
||||
// if (decode["status"] == "success") {
|
||||
// var firstElement = decode["message"][0];
|
||||
// if (double.parse(box.read(BoxName.passengerWalletTotal)) < 0) {
|
||||
// totalPassenger = totalCostPassenger -
|
||||
// (totalCostPassenger * int.parse(firstElement['amount']) / 100);
|
||||
// update();
|
||||
// } else {
|
||||
// totalPassenger = totalCostPassenger -
|
||||
// (totalCostPassenger * int.parse(firstElement['amount']) / 100);
|
||||
// update();
|
||||
// }
|
||||
|
||||
// totalDriver = totalDriver -
|
||||
// (totalDriver * int.parse(firstElement['amount']) / 100);
|
||||
// promoTaken = true;
|
||||
// update();
|
||||
// Get.back();
|
||||
// }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Reactive variable for blinking (on/off)
|
||||
var isLightOn = false.obs;
|
||||
|
||||
// To animate the border color
|
||||
var borderColor = Colors.black.obs;
|
||||
|
||||
Timer? _blinkingTimer;
|
||||
|
||||
// Method to start blinking for 5 seconds
|
||||
void startBlinking() {
|
||||
int count = 0;
|
||||
|
||||
_blinkingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
// Toggle light on/off
|
||||
isLightOn.value = !isLightOn.value;
|
||||
borderColor.value = isLightOn.value
|
||||
? Colors.yellow
|
||||
: Colors.black; // Animate border color
|
||||
|
||||
count++;
|
||||
|
||||
// Stop blinking after 5 seconds
|
||||
if (count >= 35) {
|
||||
timer.cancel();
|
||||
isLightOn.value = false; // Ensure light turns off
|
||||
borderColor.value = Colors.black; // Reset the border color
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_blinkingTimer?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
89
siro_rider/lib/controller/home/compare.sh
Normal file
89
siro_rider/lib/controller/home/compare.sh
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
|
||||
ORIG_FILE="lib/controller/home/map_passenger_controller.dart"
|
||||
ALL_FILES="lib/controller/home/map/location_search_controller.dart lib/controller/home/map/map_engine_controller.dart lib/controller/home/map/map_screen_binding.dart lib/controller/home/map/map_socket_controller.dart lib/controller/home/map/nearby_drivers_controller.dart lib/controller/home/map/ride_lifecycle_controller.dart lib/controller/home/map/ui_interactions_controller.dart"
|
||||
|
||||
echo "Extracting methods from original controller..."
|
||||
# Methods typically start with spaces and have patterns like:
|
||||
# returnType methodName( or methodName(
|
||||
# Let's extract words that precede ( on lines that don't start with keywords (if, for, while, switch, catch, etc.)
|
||||
# We will use awk to parse.
|
||||
METHODS=$(cat "$ORIG_FILE" | awk '
|
||||
# Skip single-line comments
|
||||
/\/\// { next }
|
||||
# Skip imports and class declarations
|
||||
/import/ || /class/ { next }
|
||||
# Find lines with "("
|
||||
/\(/ {
|
||||
# Replace anything inside parentheses and curly braces to simplify
|
||||
gsub(/\(.*\)/, "()")
|
||||
# Find word before "()"
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i ~ /[a-zA-Z0-9_]+\(\)/) {
|
||||
name = $i
|
||||
sub(/\(\)/, "", name)
|
||||
# Remove any leading modifiers like async, Future, static, etc.
|
||||
# Only keep valid identifiers that are not control keywords
|
||||
if (name !~ /^(if|for|while|switch|catch|super|await|print|assert|dynamic|void|return|with|override|get|set|else|try|final|const|var|late|static|factory|new|abstract|covariant|external|operator|part|required|typedef|yield)$/ && name ~ /^[a-zA-Z_][a-zA-Z0-9_]*$/) {
|
||||
print name
|
||||
}
|
||||
}
|
||||
}
|
||||
}' | sort -u)
|
||||
|
||||
echo "Extracting fields/variables from original controller..."
|
||||
# Fields are usually declared inside the class at the beginning of lines or indented.
|
||||
# e.g., RxBool isSearching = false.obs; or String? rideId;
|
||||
VARS=$(cat "$ORIG_FILE" | awk '
|
||||
/\/\// { next }
|
||||
/import/ || /class/ { next }
|
||||
# Lines ending with ";" or containing "=" followed by ";"
|
||||
/;/ {
|
||||
# Extract words that look like declarations.
|
||||
# We look for typical type names or var/final followed by variable name
|
||||
for (i = 1; i < NF; i++) {
|
||||
if ($i ~ /^(var|final|const|late|RxBool|RxInt|RxDouble|RxString|RxList|RxMap|RxSet|Rx|String|int|double|bool|List|Map|Set|Timer|LatLng|Position|IntaleqMapController)$/) {
|
||||
# The next field might be the variable name, or it might have a type like String?
|
||||
name = $(i+1)
|
||||
# Remove trailing ?, ;, =
|
||||
sub(/\?/, "", name)
|
||||
sub(/;/, "", name)
|
||||
sub(/=/, "", name)
|
||||
if (name ~ /^[a-zA-Z_][a-zA-Z0-9_]*$/) {
|
||||
print name
|
||||
}
|
||||
}
|
||||
}
|
||||
}' | sort -u)
|
||||
|
||||
echo "Checking split files for methods..."
|
||||
echo "--- MISSING METHODS ---"
|
||||
MISSING_METHODS_COUNT=0
|
||||
# Create a temporary file with all split contents to search efficiently
|
||||
cat $ALL_FILES > lib/controller/home/temp_split_combined.txt
|
||||
|
||||
for method in $METHODS; do
|
||||
# Search for this method name as a whole word in split controllers
|
||||
FOUND=$(grep -w "$method" lib/controller/home/temp_split_combined.txt 2>/dev/null)
|
||||
if [ -z "$FOUND" ]; then
|
||||
echo " - $method"
|
||||
MISSING_METHODS_COUNT=$((MISSING_METHODS_COUNT+1))
|
||||
fi
|
||||
done
|
||||
echo "Total missing methods: $MISSING_METHODS_COUNT"
|
||||
|
||||
echo ""
|
||||
echo "Checking split files for variables/fields..."
|
||||
echo "--- MISSING VARIABLES ---"
|
||||
MISSING_VARS_COUNT=0
|
||||
for var in $VARS; do
|
||||
FOUND=$(grep -w "$var" lib/controller/home/temp_split_combined.txt 2>/dev/null)
|
||||
if [ -z "$FOUND" ]; then
|
||||
echo " - $var"
|
||||
MISSING_VARS_COUNT=$((MISSING_VARS_COUNT+1))
|
||||
fi
|
||||
done
|
||||
echo "Total missing variables: $MISSING_VARS_COUNT"
|
||||
|
||||
# Clean up temp file
|
||||
rm lib/controller/home/temp_split_combined.txt
|
||||
104
siro_rider/lib/controller/home/compare_precise.py
Normal file
104
siro_rider/lib/controller/home/compare_precise.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import sys
|
||||
import re
|
||||
|
||||
def parse_stream(stream_text):
|
||||
# Splits the stream by our custom file delimiters
|
||||
files = {}
|
||||
parts = re.split(r'=== FILE: (.*?) ===\n', stream_text)
|
||||
|
||||
# The first part is the original monolithic file
|
||||
if parts:
|
||||
files['original'] = parts[0]
|
||||
|
||||
for i in range(1, len(parts), 2):
|
||||
filename = parts[i]
|
||||
content = parts[i+1] if i+1 < len(parts) else ""
|
||||
files[filename] = content
|
||||
|
||||
return files
|
||||
|
||||
def strip_comments(text):
|
||||
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
|
||||
text = re.sub(r'//.*', '', text)
|
||||
return text
|
||||
|
||||
def extract_declarations(text):
|
||||
clean = strip_comments(text)
|
||||
|
||||
# Matches method/function declarations inside a class in Dart
|
||||
# e.g., void myMethod(..., Future<void> myMethod(..., myMethod(..., get myProp, set myProp
|
||||
# We look for word followed by ( or get/set followed by word.
|
||||
method_decl_pattern = re.compile(
|
||||
r'(?:[a-zA-Z0-9_<>\?\[\]]+(?:\s+[a-zA-Z0-9_<>\?\[\]]+)*\s+)?([a-zA-Z0-9_]+)\s*\([^\)]*\)\s*(?:async)?\s*(?:=>|\{)'
|
||||
)
|
||||
|
||||
methods = set()
|
||||
for match in method_decl_pattern.finditer(clean):
|
||||
method_name = match.group(1)
|
||||
if method_name not in keywords and not method_name.isdigit():
|
||||
methods.add(method_name)
|
||||
|
||||
# Also extract getters and setters
|
||||
getset_pattern = re.compile(r'\b(?:get|set)\s+([a-zA-Z0-9_]+)\b')
|
||||
for match in getset_pattern.finditer(clean):
|
||||
name = match.group(1)
|
||||
if name not in keywords:
|
||||
methods.add(name)
|
||||
|
||||
# Extract variables/fields declarations
|
||||
var_decl_pattern = re.compile(
|
||||
r'\b(?:var|final|const|late|RxBool|RxInt|RxDouble|RxString|RxList|RxMap|RxSet|Rx|String|int|double|bool|List|Map|Set|Timer|LatLng|Position|IntaleqMapController)\??\s+([a-zA-Z0-9_]+)\b'
|
||||
)
|
||||
|
||||
variables = set()
|
||||
for match in var_decl_pattern.finditer(clean):
|
||||
var_name = match.group(1)
|
||||
if var_name not in keywords and not var_name.isdigit():
|
||||
variables.add(var_name)
|
||||
|
||||
return methods, variables
|
||||
|
||||
keywords = {
|
||||
'if', 'for', 'while', 'switch', 'catch', 'super', 'await', 'print',
|
||||
'assert', 'dynamic', 'void', 'return', 'with', 'override', 'get', 'set',
|
||||
'class', 'import', 'extends', 'implements', 'mixin', 'this', 'else', 'try',
|
||||
'final', 'const', 'var', 'late', 'static', 'factory', 'new', 'abstract',
|
||||
'covariant', 'external', 'operator', 'part', 'required', 'typedef', 'yield'
|
||||
}
|
||||
|
||||
def main():
|
||||
stream_text = sys.stdin.read()
|
||||
files = parse_stream(stream_text)
|
||||
|
||||
orig_content = files.get('original', '')
|
||||
split_contents = {k: v for k, v in files.items() if k != 'original'}
|
||||
|
||||
orig_methods, orig_vars = extract_declarations(orig_content)
|
||||
|
||||
# Combined declarations in split files
|
||||
split_methods = set()
|
||||
split_vars = set()
|
||||
for filename, content in split_contents.items():
|
||||
m, v = extract_declarations(content)
|
||||
split_methods.update(m)
|
||||
split_vars.update(v)
|
||||
|
||||
missing_methods = sorted(orig_methods - split_methods)
|
||||
missing_vars = sorted(orig_vars - split_vars)
|
||||
|
||||
print("--- PRECISE MISSING METHODS ---")
|
||||
print(f"Total original methods/getters/setters: {len(orig_methods)}")
|
||||
print(f"Total defined in split controllers: {len(split_methods)}")
|
||||
print(f"Total missing: {len(missing_methods)}")
|
||||
for m in missing_methods:
|
||||
print(f" - {m}")
|
||||
|
||||
print("\n--- PRECISE MISSING VARIABLES/FIELDS ---")
|
||||
print(f"Total original variables: {len(orig_vars)}")
|
||||
print(f"Total defined in split controllers: {len(split_vars)}")
|
||||
print(f"Total missing: {len(missing_vars)}")
|
||||
for v in missing_vars:
|
||||
print(f" - {v}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
103
siro_rider/lib/controller/home/comparison_results.txt
Normal file
103
siro_rider/lib/controller/home/comparison_results.txt
Normal file
@@ -0,0 +1,103 @@
|
||||
Extracting methods from original controller...
|
||||
Extracting fields/variables from original controller...
|
||||
Checking split files for methods...
|
||||
--- MISSING METHODS ---
|
||||
- _applyLowEndModeIfNeeded
|
||||
- _buildOsrmWaypointCoords
|
||||
- _calculateDistance
|
||||
- _checkAndRecalculateIfDeviated
|
||||
- _fillDriverDataLocally
|
||||
- _haversineKm
|
||||
- _initMinimalIcons
|
||||
- _initializePolygons
|
||||
- _isActiveRideState
|
||||
- _kmToLatDelta
|
||||
- _kmToLngDelta
|
||||
- _onDriverArrivedWithSocket
|
||||
- _onRideCancelledWithSocket
|
||||
- _onRideStartedWithSocket
|
||||
- _relevanceScore
|
||||
- _restorePolyline
|
||||
- _stageNiceToHave
|
||||
- _stagePricingAndState
|
||||
- _startMasterTimer
|
||||
- _startMasterTimerWithInterval
|
||||
- _startPollingFallback
|
||||
- _stopDriverLocationPolling
|
||||
- _updateDriverMarker
|
||||
- cancelRide
|
||||
- detectPerfMode
|
||||
- getAIKey
|
||||
- getMapPointsForAllMethods
|
||||
- getPassengerLocationUniversity
|
||||
- handleActiveRideOnStartup
|
||||
- isDriversDataValid
|
||||
- onChangedPassengerCount
|
||||
- onChangedPassengersChoose
|
||||
- showDrawingBottomSheet
|
||||
- showNoDriversDialog
|
||||
- startSearchingTimer
|
||||
Total missing methods: 35
|
||||
|
||||
Checking split files for variables/fields...
|
||||
--- MISSING VARIABLES ---
|
||||
- _isStateProcessing
|
||||
- _isUsingFallback
|
||||
- _maxReconnectAttempts
|
||||
- apiDistanceMeters
|
||||
- c
|
||||
- carInfo
|
||||
- carsOrder
|
||||
- coordDestination
|
||||
- currentCarType
|
||||
- currentDriverLocation
|
||||
- currentLocationOfDrivers
|
||||
- currentRideId
|
||||
- currentTimeSearchingCaptainWindow
|
||||
- dInfo
|
||||
- dLat
|
||||
- datadriverCarsLocationToPassengerAfterApplied
|
||||
- distanceOfTrip
|
||||
- driverCarPlate
|
||||
- driverLocationToPassenger
|
||||
- driverOrderStatus
|
||||
- durationByPassenger
|
||||
- endLocation
|
||||
- fName
|
||||
- finalReason
|
||||
- headingList
|
||||
- increaseFeeFormKey
|
||||
- isDriversTokensSend
|
||||
- isFirstWaypoint
|
||||
- isInUniversity
|
||||
- isSaaSRequest
|
||||
- kmInDegree
|
||||
- lName
|
||||
- latDest
|
||||
- latestPosition
|
||||
- lngDest
|
||||
- lowPerf
|
||||
- messagesFormKey
|
||||
- originCoords
|
||||
- pLower
|
||||
- passengerLocationStringUnvirsity
|
||||
- previousLocationOfDrivers
|
||||
- progressTimerRideBeginVip
|
||||
- qLower
|
||||
- rLat1
|
||||
- rLat2
|
||||
- ram
|
||||
- rideData
|
||||
- sdk
|
||||
- selectedPassengerCount
|
||||
- startLng
|
||||
- startLocation
|
||||
- stringElapsedTimeRideBegin
|
||||
- tax
|
||||
- totalPassengerBalashDiscount
|
||||
- totalPassengerComfortDiscount
|
||||
- totalPassengerElectricDiscount
|
||||
- totalPassengerLadyDiscount
|
||||
- totalPassengerRaihGaiDiscount
|
||||
- totalPassengerSpeedDiscount
|
||||
Total missing variables: 59
|
||||
121
siro_rider/lib/controller/home/contact_us_controller.dart
Normal file
121
siro_rider/lib/controller/home/contact_us_controller.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../functions/launch.dart';
|
||||
|
||||
class ContactUsController extends GetxController {
|
||||
/// WORKING HOURS (10:00 → 16:00)
|
||||
final TimeOfDay workStartTime = const TimeOfDay(hour: 10, minute: 0);
|
||||
final TimeOfDay workEndTime = const TimeOfDay(hour: 16, minute: 0);
|
||||
|
||||
bool get isWorkTime {
|
||||
final now = TimeOfDay.now();
|
||||
return (now.hour > workStartTime.hour ||
|
||||
(now.hour == workStartTime.hour &&
|
||||
now.minute >= workStartTime.minute)) &&
|
||||
(now.hour < workEndTime.hour ||
|
||||
(now.hour == workEndTime.hour && now.minute <= workEndTime.minute));
|
||||
}
|
||||
|
||||
/// Helper to format working hours for UI
|
||||
String get workHoursString =>
|
||||
'${workStartTime.hour.toString().padLeft(2, '0')}:${workStartTime.minute.toString().padLeft(2, '0')} - '
|
||||
'${workEndTime.hour.toString().padLeft(2, '0')}:${workEndTime.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
/// PHONE LIST (USED FOR CALLS + WHATSAPP)
|
||||
final List<String> phoneNumbers = [
|
||||
'+963952475734',
|
||||
'+963952475740',
|
||||
'+963952475742'
|
||||
];
|
||||
|
||||
/// RANDOM PHONE SELECTOR
|
||||
String getRandomPhone() {
|
||||
final random = Random();
|
||||
return phoneNumbers[random.nextInt(phoneNumbers.length)];
|
||||
}
|
||||
|
||||
/// DIRECT ACTIONS
|
||||
void makeCall() {
|
||||
if (isWorkTime) {
|
||||
makePhoneCall(getRandomPhone());
|
||||
}
|
||||
}
|
||||
|
||||
void sendWhatsApp() {
|
||||
launchCommunication('whatsapp', getRandomPhone(), 'Hello'.tr);
|
||||
}
|
||||
|
||||
void sendEmail() {
|
||||
launchCommunication('email', 'support@intaleqapp.com', 'Hello'.tr);
|
||||
}
|
||||
|
||||
/// SHOW DIALOG (Optional legacy support)
|
||||
void showContactDialog(BuildContext context) {
|
||||
bool withinHours = isWorkTime;
|
||||
|
||||
showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (context) => CupertinoActionSheet(
|
||||
title: Text('Contact Us'.tr),
|
||||
message: Text('Choose a contact option'.tr),
|
||||
actions: <Widget>[
|
||||
if (withinHours)
|
||||
CupertinoActionSheetAction(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Icon(CupertinoIcons.phone),
|
||||
Text('Call Support'.tr),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
makeCall();
|
||||
},
|
||||
),
|
||||
if (!withinHours)
|
||||
CupertinoActionSheetAction(
|
||||
child: Text(
|
||||
'Work time is from 10:00 AM to 16:00 PM.\nYou can send a WhatsApp message or email.'
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
const Icon(
|
||||
FontAwesome.whatsapp,
|
||||
color: AppColor.greenColor,
|
||||
),
|
||||
Text('Send WhatsApp Message'.tr),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
sendWhatsApp();
|
||||
},
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
child: Text('Send Email'.tr),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
sendEmail();
|
||||
},
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
child: Text('Cancel'.tr),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
62
siro_rider/lib/controller/home/decode_polyline_isolate.dart
Normal file
62
siro_rider/lib/controller/home/decode_polyline_isolate.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
// Helper method for decoding polyline (if not already defined)
|
||||
// List<LatLng> decodePolyline(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;
|
||||
// }
|
||||
43
siro_rider/lib/controller/home/deep_link_controller.dart
Normal file
43
siro_rider/lib/controller/home/deep_link_controller.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:siro_rider/print.dart';
|
||||
import 'dart:async';
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class DeepLinkController extends GetxController {
|
||||
final _appLinks = AppLinks();
|
||||
StreamSubscription<Uri>? _linkSubscription;
|
||||
|
||||
// تخزين الرابط الخام (URL) ليتم معالجته لاحقاً في MapPassengerController
|
||||
final Rx<String?> rawDeepLink = Rx<String?>(null);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
initDeepLinks();
|
||||
}
|
||||
|
||||
Future<void> initDeepLinks() async {
|
||||
// الاستماع للروابط والتطبيق يعمل في الخلفية
|
||||
_linkSubscription = _appLinks.uriLinkStream.listen((uri) {
|
||||
Log.print('🔗 Received deep link (Stream): $uri');
|
||||
rawDeepLink.value = uri.toString();
|
||||
});
|
||||
|
||||
// الاستماع للروابط إذا كان التطبيق مغلقاً تماماً (Cold Start)
|
||||
try {
|
||||
final initialUri = await _appLinks.getInitialLink();
|
||||
if (initialUri != null) {
|
||||
Log.print('🔗 Received initial deep link (Cold Start): $initialUri');
|
||||
rawDeepLink.value = initialUri.toString();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('Error getting initial link: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_linkSubscription?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
127
siro_rider/lib/controller/home/device_performance.dart
Normal file
127
siro_rider/lib/controller/home/device_performance.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
|
||||
class DevicePerformanceManager {
|
||||
/// القائمة البيضاء لموديلات الهواتف القوية (Flagships Only)
|
||||
/// أي هاتف يبدأ موديله بأحد هذه الرموز سيعتبر قوياً
|
||||
static const List<String> _highEndSamsungModels = [
|
||||
'SM-S', // سلسلة Galaxy S21, S22, S23, S24 (S901, S908, S911...)
|
||||
'SM-F', // سلسلة Fold و Flip (Z Fold, Z Flip)
|
||||
'SM-N9', // سلسلة Note 9, Note 10, Note 20
|
||||
'SM-G9', // سلسلة S10, S20 (G970, G980...)
|
||||
];
|
||||
|
||||
static const List<String> _highEndGoogleModels = [
|
||||
'Pixel 6',
|
||||
'Pixel 7',
|
||||
'Pixel 8',
|
||||
'Pixel 9',
|
||||
'Pixel Fold'
|
||||
];
|
||||
|
||||
static const List<String> _highEndHuaweiModels = [
|
||||
'ELS-', // P40 Pro
|
||||
'ANA-', // P40
|
||||
'HMA-', // Mate 20
|
||||
'LYA-', // Mate 20 Pro
|
||||
'VOG-', // P30 Pro
|
||||
'ELE-', // P30
|
||||
'NOH-', // Mate 40 Pro
|
||||
'AL00', // Mate X series (some)
|
||||
];
|
||||
|
||||
static const List<String> _highEndXiaomiModels = [
|
||||
'2201122', // Xiaomi 12 series patterns often look like this
|
||||
'2210132', // Xiaomi 13
|
||||
'2304FPN', // Xiaomi 13 Ultra
|
||||
'M2007J1', // Mi 10 series
|
||||
'M2102K1', // Mi 11 Ultra
|
||||
];
|
||||
|
||||
static const List<String> _highEndOnePlusModels = [
|
||||
'GM19', // OnePlus 7
|
||||
'HD19', // OnePlus 7T
|
||||
'IN20', // OnePlus 8
|
||||
'KB20', // OnePlus 8T
|
||||
'LE21', // OnePlus 9
|
||||
'NE22', // OnePlus 10
|
||||
'PHB110', // OnePlus 11
|
||||
'CPH', // Newer OnePlus models
|
||||
];
|
||||
|
||||
/// دالة الفحص الرئيسية
|
||||
static Future<bool> isHighEndDevice() async {
|
||||
// 1. الآيفون دائماً قوي (نظام الرسوميات فيه متفوق حتى في الموديلات القديمة)
|
||||
if (Platform.isIOS) return true;
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
|
||||
String manufacturer = androidInfo.manufacturer.toLowerCase();
|
||||
String model =
|
||||
androidInfo.model.toUpperCase(); // نحوله لحروف كبيرة للمقارنة
|
||||
String hardware = androidInfo.hardware.toLowerCase(); // المعالج
|
||||
|
||||
// --- الفحص العكسي (الحظر المباشر) ---
|
||||
// إذا كان المعالج من الفئات الضعيفة جداً المشهورة في الهواتف المقلدة
|
||||
// mt65xx, mt6735, sc77xx هي معالجات رخيصة جداً
|
||||
if (hardware.contains('mt65') ||
|
||||
hardware.contains('mt6735') ||
|
||||
hardware.contains('sc77')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- فحص القائمة البيضاء (Whitelist) ---
|
||||
|
||||
// 1. Samsung Flagships
|
||||
if (manufacturer.contains('samsung')) {
|
||||
for (var prefix in _highEndSamsungModels) {
|
||||
if (model.startsWith(prefix)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Google Pixel (6 and above)
|
||||
if (manufacturer.contains('google')) {
|
||||
for (var prefix in _highEndGoogleModels) {
|
||||
if (model.contains(prefix.toUpperCase())) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Huawei Flagships
|
||||
if (manufacturer.contains('huawei')) {
|
||||
for (var prefix in _highEndHuaweiModels) {
|
||||
if (model.startsWith(prefix)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. OnePlus Flagships
|
||||
if (manufacturer.contains('oneplus')) {
|
||||
for (var prefix in _highEndOnePlusModels) {
|
||||
if (model.startsWith(prefix)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Xiaomi Flagships
|
||||
if (manufacturer.contains('xiaomi') ||
|
||||
manufacturer.contains('redmi') ||
|
||||
manufacturer.contains('poco')) {
|
||||
// شاومي تسميتها معقدة، لذا سنعتمد على فحص الرام كعامل مساعد هنا فقط
|
||||
// لأن هواتف شاومي القوية عادة لا تزور الرام
|
||||
// الرام يجب أن يكون أكبر من 6 جيجا (بايت)
|
||||
double ramGB = (androidInfo.availableRamSize) / (1024 * 1024 * 1024);
|
||||
if (ramGB > 7.5)
|
||||
return true; // 8GB RAM or more is usually safe for Xiaomi high-end
|
||||
}
|
||||
|
||||
// إذا لم يكن من ضمن القوائم أعلاه، نعتبره جهازاً متوسطاً/ضعيفاً ونعرض الرسم البسيط
|
||||
return false;
|
||||
} catch (e) {
|
||||
// في حال حدوث خطأ في الفحص، نعود للوضع الآمن (الرسم البسيط)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
86
siro_rider/lib/controller/home/device_tier.dart
Normal file
86
siro_rider/lib/controller/home/device_tier.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
|
||||
// مفاتيح التخزين (بسيطة)
|
||||
const _kDeviceTierKey = 'deviceTier'; // 'low' | 'mid' | 'high'
|
||||
const _kDeviceTierCheckedAtKey = 'deviceTierTime'; // millisSinceEpoch
|
||||
|
||||
Future<String> detectAndCacheDeviceTier({bool force = false}) async {
|
||||
// لا تعيد الفحص إذا عملناه خلال آخر 24 ساعة
|
||||
if (!force) {
|
||||
final last = box.read(_kDeviceTierCheckedAtKey);
|
||||
if (last is int) {
|
||||
final dt = DateTime.fromMillisecondsSinceEpoch(last);
|
||||
if (DateTime.now().difference(dt) < const Duration(hours: 24)) {
|
||||
final cached = box.read(_kDeviceTierKey);
|
||||
if (cached is String && cached.isNotEmpty)
|
||||
return cached; // low/mid/high
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final info = DeviceInfoPlugin();
|
||||
int score = 0;
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
final a = await info.androidInfo;
|
||||
final int sdk = a.version.sdkInt ?? 0;
|
||||
final int cores = Platform.numberOfProcessors;
|
||||
final int abisCount = a.supportedAbis.length;
|
||||
final bool isEmu = !(a.isPhysicalDevice ?? true);
|
||||
|
||||
// SDK (أقدم = أضعف)
|
||||
if (sdk <= 26)
|
||||
score += 3; // 8.0 وأقدم
|
||||
else if (sdk <= 29)
|
||||
score += 2; // 9-10
|
||||
else if (sdk <= 30) score += 1; // 11
|
||||
|
||||
// الأنوية
|
||||
if (cores <= 4)
|
||||
score += 3;
|
||||
else if (cores <= 6)
|
||||
score += 2;
|
||||
else if (cores <= 8) score += 1;
|
||||
|
||||
// ABI count (القليل غالباً أضعف)
|
||||
if (abisCount <= 1)
|
||||
score += 2;
|
||||
else if (abisCount == 2) score += 1;
|
||||
|
||||
// محاكي
|
||||
if (isEmu) score += 2;
|
||||
} else {
|
||||
// iOS/منصات أخرى: تقدير سريع بالأنوية فقط
|
||||
final int cores = Platform.numberOfProcessors;
|
||||
if (cores <= 4)
|
||||
score += 3;
|
||||
else if (cores <= 6)
|
||||
score += 2;
|
||||
else if (cores <= 8) score += 1;
|
||||
}
|
||||
|
||||
// تحويل السكور إلى تصنيف
|
||||
final String tier = (score >= 6)
|
||||
? 'low'
|
||||
: (score >= 3)
|
||||
? 'mid'
|
||||
: 'high';
|
||||
|
||||
box.write(_kDeviceTierKey, tier);
|
||||
box.write(_kDeviceTierCheckedAtKey, DateTime.now().millisecondsSinceEpoch);
|
||||
return tier;
|
||||
}
|
||||
|
||||
// للقراءة السريعة وقت ما تحتاج:
|
||||
String getCachedDeviceTier() {
|
||||
final t = box.read(_kDeviceTierKey);
|
||||
if (t is String && t.isNotEmpty) return t;
|
||||
return 'mid';
|
||||
}
|
||||
|
||||
bool isLowEnd() => getCachedDeviceTier() == 'low';
|
||||
bool isMidEnd() => getCachedDeviceTier() == 'mid';
|
||||
bool isHighEnd() => getCachedDeviceTier() == 'high';
|
||||
15
siro_rider/lib/controller/home/home_page_controller.dart
Normal file
15
siro_rider/lib/controller/home/home_page_controller.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../main.dart';
|
||||
|
||||
class HomePageController extends GetxController {
|
||||
late bool isVibrate = box.read(BoxName.isvibrate) ?? true;
|
||||
|
||||
void changeVibrateOption(bool value) {
|
||||
isVibrate = box.read(BoxName.isvibrate) ?? true;
|
||||
isVibrate = value;
|
||||
box.write(BoxName.isvibrate, value);
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:siro_rider/print.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:live_activities/live_activities.dart';
|
||||
|
||||
class IosLiveActivityService {
|
||||
static final _liveActivitiesPlugin = LiveActivities();
|
||||
static String? _activityId;
|
||||
|
||||
static void init() {
|
||||
if (Platform.isIOS) {
|
||||
_liveActivitiesPlugin.init(
|
||||
appGroupId: "group.com.Intaleq.intaleq",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> startRideActivity({
|
||||
required String rideId,
|
||||
required String driverName,
|
||||
required String carDetails,
|
||||
required String etaText,
|
||||
required double progress,
|
||||
}) async {
|
||||
if (!Platform.isIOS) return;
|
||||
|
||||
try {
|
||||
await _liveActivitiesPlugin.endAllActivities();
|
||||
|
||||
// استخدام dynamic يسمح للحزمة بحفظ النصوص والأرقام في الذاكرة المشتركة
|
||||
final Map<String, dynamic> activityModel = {
|
||||
'status': 'waiting',
|
||||
'driverName': driverName,
|
||||
'carDetails': carDetails,
|
||||
'etaText': etaText,
|
||||
'progress': progress,
|
||||
};
|
||||
|
||||
// الدالة هنا تأخذ المعاملات مباشرة
|
||||
_activityId = await _liveActivitiesPlugin.createActivity(
|
||||
rideId,
|
||||
activityModel,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.print("❌ Live Activity Start Error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> updateRideActivity({
|
||||
required String status,
|
||||
required String driverName,
|
||||
required String carDetails,
|
||||
required String etaText,
|
||||
required double progress,
|
||||
}) async {
|
||||
if (!Platform.isIOS || _activityId == null) return;
|
||||
|
||||
final Map<String, dynamic> updatedModel = {
|
||||
'status': status,
|
||||
'driverName': driverName,
|
||||
'carDetails': carDetails,
|
||||
'etaText': etaText,
|
||||
'progress': progress,
|
||||
};
|
||||
|
||||
await _liveActivitiesPlugin.updateActivity(_activityId!, updatedModel);
|
||||
}
|
||||
|
||||
static Future<void> endRideActivity() async {
|
||||
if (!Platform.isIOS || _activityId == null) return;
|
||||
await _liveActivitiesPlugin.endActivity(_activityId!);
|
||||
_activityId = null;
|
||||
}
|
||||
}
|
||||
15
siro_rider/lib/controller/home/map/car_location.dart
Normal file
15
siro_rider/lib/controller/home/map/car_location.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
class CarLocation {
|
||||
final String id;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final double distance;
|
||||
final double duration;
|
||||
|
||||
CarLocation({
|
||||
required this.id,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
this.distance = 10000,
|
||||
this.duration = 10000,
|
||||
});
|
||||
}
|
||||
1052
siro_rider/lib/controller/home/map/location_search_controller.dart
Normal file
1052
siro_rider/lib/controller/home/map/location_search_controller.dart
Normal file
File diff suppressed because it is too large
Load Diff
809
siro_rider/lib/controller/home/map/map_engine_controller.dart
Normal file
809
siro_rider/lib/controller/home/map/map_engine_controller.dart
Normal file
@@ -0,0 +1,809 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' show cos, max, min, pi, pow, sqrt;
|
||||
import 'dart:typed_data';
|
||||
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
// contains global 'box'
|
||||
import '../../../print.dart';
|
||||
import '../../../views/home/map_widget.dart/cancel_raide_page.dart';
|
||||
import 'location_search_controller.dart';
|
||||
import 'nearby_drivers_controller.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import '../points_for_rider_controller.dart';
|
||||
import '../../../constant/univeries_polygon.dart';
|
||||
|
||||
class MapEngineController extends GetxController {
|
||||
IntaleqMapController? mapController;
|
||||
bool isStyleLoaded = false;
|
||||
bool isIconsLoaded = false;
|
||||
|
||||
Set<Marker> markers = {};
|
||||
Set<Polyline> polyLines = {};
|
||||
List<LatLng> polylineCoordinates = [];
|
||||
Set<Polygon> polygons = {};
|
||||
Set<Circle> circles = {};
|
||||
|
||||
LatLngBounds? lastComputedBounds;
|
||||
bool mapType = false;
|
||||
bool mapTrafficON = false;
|
||||
bool isMarkersShown = false;
|
||||
|
||||
String markerIcon = "marker_icon";
|
||||
String tripIcon = "trip_icon";
|
||||
String startIcon = "start_icon";
|
||||
String endIcon = "end_icon";
|
||||
String carIcon = "car_icon";
|
||||
String motoIcon = "moto_icon";
|
||||
String ladyIcon = "lady_icon";
|
||||
|
||||
double height = 150;
|
||||
double heightMenu = 0;
|
||||
double widthMenu = 0;
|
||||
double heightPickerContainer = 90;
|
||||
double heightPointsPageForRider = 0;
|
||||
double mainBottomMenuMapHeight = Get.height * .2;
|
||||
double wayPointSheetHeight = 0;
|
||||
bool heightMenuBool = false;
|
||||
bool isPickerShown = false;
|
||||
bool isPointsPageForRider = false;
|
||||
bool isBottomSheetShown = false;
|
||||
bool reloadStartApp = false;
|
||||
bool isCancelRidePageShown = false;
|
||||
bool isCashConfirmPageShown = false;
|
||||
bool isPaymentMethodPageShown = false;
|
||||
bool isRideFinished = false;
|
||||
bool rideConfirm = false;
|
||||
bool isMainBottomMenuMap = true;
|
||||
|
||||
bool isWayPointSheet = false;
|
||||
bool isWayPointStopsSheet = false;
|
||||
bool isWayPointStopsSheetUtilGetMap = false;
|
||||
double heightBottomSheetShown = 0;
|
||||
double cashConfirmPageShown = 250;
|
||||
double widthMapTypeAndTraffic = 50;
|
||||
double paymentPageShown = Get.height * .6;
|
||||
|
||||
bool isAnotherOreder = false;
|
||||
bool isWhatsAppOrder = false;
|
||||
|
||||
Map<String, Timer> _animationTimers = {};
|
||||
final int updateIntervalMs = 100;
|
||||
final double minMovementThreshold = 1.0;
|
||||
|
||||
void onMapCreated(IntaleqMapController controller) {
|
||||
mapController = controller;
|
||||
update();
|
||||
}
|
||||
|
||||
void onStyleLoaded() async {
|
||||
Log.print('🗺️ Intaleq Map Style Loaded. Initializing...');
|
||||
isStyleLoaded = true;
|
||||
await _loadMapIcons();
|
||||
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
Get.find<RideLifecycleController>().reinit();
|
||||
|
||||
if (mapController != null) {
|
||||
if (markers.isNotEmpty && lastComputedBounds != null) {
|
||||
await _safeAnimateCameraBounds(lastComputedBounds);
|
||||
} else {
|
||||
mapController!.animateCamera(
|
||||
CameraUpdate.newLatLng(locationSearch.passengerLocation),
|
||||
);
|
||||
}
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> _safeAnimateCameraBounds(LatLngBounds? bounds,
|
||||
{double left = 60,
|
||||
double top = 60,
|
||||
double right = 60,
|
||||
double bottom = 60}) async {
|
||||
if (bounds == null || mapController == null) return;
|
||||
|
||||
try {
|
||||
if (bounds.northeast.latitude == bounds.southwest.latitude &&
|
||||
bounds.northeast.longitude == bounds.southwest.longitude) {
|
||||
Log.print(
|
||||
'⚠️ _safeAnimateCameraBounds: Bounds are a single point, zooming to point instead.');
|
||||
await mapController
|
||||
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 15));
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
await mapController?.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(
|
||||
bounds,
|
||||
left: left,
|
||||
top: top,
|
||||
right: right,
|
||||
bottom: bottom,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.print('❌ _safeAnimateCameraBounds CRASH PREVENTED: $e');
|
||||
try {
|
||||
await mapController
|
||||
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 14));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMapIcons() async {
|
||||
isIconsLoaded = false;
|
||||
for (int i = 0; i < 15; i++) {
|
||||
if (mapController != null && isStyleLoaded) break;
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
if (mapController == null || !isStyleLoaded) {
|
||||
Log.print(
|
||||
'⚠️ _loadMapIcons: mapController or style not ready. Icons may not load.');
|
||||
}
|
||||
|
||||
await _addMapImage(startIcon, 'assets/images/A.png');
|
||||
await _addMapImage(endIcon, 'assets/images/b.png');
|
||||
await _addMapImage(carIcon, 'assets/images/car.png');
|
||||
await _addMapImage(motoIcon, 'assets/images/moto.png');
|
||||
await _addMapImage(ladyIcon, 'assets/images/lady.png');
|
||||
await _addMapImage('picker_icon', 'assets/images/picker.png');
|
||||
await _addMapImage('orange_marker', 'assets/images/moto1.png');
|
||||
await _addMapImage('violet_marker', 'assets/images/lady1.png');
|
||||
|
||||
isIconsLoaded = true;
|
||||
markers = markers.map((m) => m.copyWith()).toSet();
|
||||
update();
|
||||
|
||||
if (Get.isRegistered<NearbyDriversController>()) {
|
||||
Get.find<NearbyDriversController>()
|
||||
.getCarsLocationByPassengerAndReloadMarker();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addMapImage(String id, String path) async {
|
||||
try {
|
||||
final ByteData bytes = await rootBundle.load(path);
|
||||
final size = _getImageSize(id);
|
||||
if (size != null && (id == carIcon || id == motoIcon || id == ladyIcon)) {
|
||||
final resized = await _resizeImage(bytes.buffer.asUint8List(), size);
|
||||
await mapController?.addImage(id, resized);
|
||||
Log.print(
|
||||
'delimited: successfully added resized map image: $id (${size}x${size})');
|
||||
} else {
|
||||
await mapController?.addImage(id, bytes.buffer.asUint8List());
|
||||
Log.print('delimited: successfully added map image: $id');
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('❌ Error loading map icon $id: $e');
|
||||
}
|
||||
}
|
||||
|
||||
int? _getImageSize(String id) {
|
||||
if (id == carIcon || id == motoIcon || id == ladyIcon) return 120;
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Uint8List> _resizeImage(Uint8List bytes, int size) async {
|
||||
return await compute((Uint8List data) {
|
||||
final image = img.decodeImage(data);
|
||||
if (image == null) return data;
|
||||
final resized = img.copyResize(image, width: size, height: size);
|
||||
return Uint8List.fromList(img.encodePng(resized));
|
||||
}, bytes);
|
||||
}
|
||||
|
||||
void clearPolyline() {
|
||||
polyLines.clear();
|
||||
update();
|
||||
}
|
||||
|
||||
LatLngBounds calculateBounds(double lat, double lng, double radiusInMeters) {
|
||||
const double earthRadius = 6378137.0;
|
||||
|
||||
double latDelta = (radiusInMeters / earthRadius) * (180 / pi);
|
||||
double lngDelta =
|
||||
(radiusInMeters / (earthRadius * cos(pi * lat / 180))) * (180 / pi);
|
||||
|
||||
double minLat = lat - latDelta;
|
||||
double maxLat = lat + latDelta;
|
||||
|
||||
double minLng = lng - lngDelta;
|
||||
double maxLng = lng + lngDelta;
|
||||
|
||||
minLat = max(-90.0, minLat);
|
||||
maxLat = min(90.0, maxLat);
|
||||
|
||||
minLng = (minLng + 180) % 360 - 180;
|
||||
maxLng = (maxLng + 180) % 360 - 180;
|
||||
|
||||
if (minLng > maxLng) {
|
||||
double temp = minLng;
|
||||
minLng = maxLng;
|
||||
maxLng = temp;
|
||||
}
|
||||
|
||||
return LatLngBounds(
|
||||
southwest: LatLng(minLat, minLng),
|
||||
northeast: LatLng(maxLat, maxLng),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> playRouteAnimation(
|
||||
List<LatLng> coords, LatLngBounds? bounds) async {
|
||||
const List<Color> segmentColors = [
|
||||
Color(0xFF109642), // Green
|
||||
Color(0xFFF59E0B), // Amber
|
||||
Color(0xFF7C3AED), // Purple
|
||||
Color(0xFFEF4444), // Red
|
||||
];
|
||||
|
||||
Set<Polyline> newPolylines = {};
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (locationSearch.activeMenuWaypointCount > 0) {
|
||||
List<int> splitIndices = [];
|
||||
for (int w = 0; w < locationSearch.activeMenuWaypointCount; w++) {
|
||||
final wp = locationSearch.menuWaypoints[w];
|
||||
if (wp == null) continue;
|
||||
int bestIdx = 0;
|
||||
double bestDist = double.infinity;
|
||||
for (int j = 0; j < coords.length; j++) {
|
||||
final dx = coords[j].latitude - wp.latitude;
|
||||
final dy = coords[j].longitude - wp.longitude;
|
||||
final d = dx * dx + dy * dy;
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
bestIdx = j;
|
||||
}
|
||||
}
|
||||
splitIndices.add(bestIdx);
|
||||
}
|
||||
splitIndices.sort();
|
||||
|
||||
List<int> boundaries = [0, ...splitIndices, coords.length - 1];
|
||||
for (int s = 0; s < boundaries.length - 1; s++) {
|
||||
int from = boundaries[s];
|
||||
int to = boundaries[s + 1] + 1;
|
||||
if (to > coords.length) to = coords.length;
|
||||
if (from >= to - 1) continue;
|
||||
final segCoords = coords.sublist(from, to);
|
||||
if (segCoords.length < 2) continue;
|
||||
final color = segmentColors[s % segmentColors.length];
|
||||
|
||||
newPolylines.add(Polyline(
|
||||
polylineId: PolylineId('segment_$s'),
|
||||
points: segCoords,
|
||||
color: color,
|
||||
width: 6,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
newPolylines.add(Polyline(
|
||||
polylineId: const PolylineId('route_primary'),
|
||||
points: coords,
|
||||
color: AppColor.primaryColor,
|
||||
width: 6,
|
||||
));
|
||||
}
|
||||
|
||||
polyLines = newPolylines;
|
||||
update();
|
||||
|
||||
Log.print(
|
||||
'🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map');
|
||||
|
||||
if (bounds != null) {
|
||||
await _safeAnimateCameraBounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
void _fitCameraToPoints(LatLng p1, LatLng p2) async {
|
||||
if (mapController == null) return;
|
||||
|
||||
if (p1.latitude == p2.latitude && p1.longitude == p2.longitude) {
|
||||
try {
|
||||
mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 17));
|
||||
} catch (e) {
|
||||
Log.print("Error animating to single point: $e");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
double minLat = min(p1.latitude, p2.latitude);
|
||||
double maxLat = max(p1.latitude, p2.latitude);
|
||||
double minLng = min(p1.longitude, p2.longitude);
|
||||
double maxLng = max(p1.longitude, p2.longitude);
|
||||
|
||||
if ((maxLat - minLat).abs() < 0.002 && (maxLng - minLng).abs() < 0.002) {
|
||||
try {
|
||||
mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 16));
|
||||
} catch (e) {
|
||||
Log.print("Error animating to single point: $e");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
double padding = 50.0;
|
||||
|
||||
try {
|
||||
await mapController?.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(
|
||||
LatLngBounds(
|
||||
southwest: LatLng(minLat, minLng),
|
||||
northeast: LatLng(maxLat, maxLng),
|
||||
),
|
||||
left: padding,
|
||||
top: padding,
|
||||
right: padding,
|
||||
bottom: padding,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.print("Error animating bounds: $e");
|
||||
try {
|
||||
LatLng center = LatLng((minLat + maxLat) / 2, (minLng + maxLng) / 2);
|
||||
mapController?.animateCamera(CameraUpdate.newLatLngZoom(center, 14));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
void fitCameraToPoints(LatLng p1, LatLng p2) {
|
||||
_fitCameraToPoints(p1, p2);
|
||||
}
|
||||
|
||||
void clearMarkersExceptStartEndAndDriver() {
|
||||
const String currentDriverMarkerId = 'assigned_driver_marker';
|
||||
markers.removeWhere((marker) {
|
||||
String id = marker.markerId.value;
|
||||
if (id == 'start') return false;
|
||||
if (id == 'end') return false;
|
||||
if (id == currentDriverMarkerId) return false;
|
||||
return true;
|
||||
});
|
||||
update();
|
||||
}
|
||||
|
||||
void clearMarkersExceptStartEnd() {
|
||||
markers.removeWhere((marker) {
|
||||
String id = marker.markerId.value;
|
||||
return id != 'start' && id != 'end';
|
||||
});
|
||||
update();
|
||||
}
|
||||
|
||||
void _updateMarkerPosition(
|
||||
LatLng newPosition, double newHeading, String icon) {
|
||||
const String markerId = 'driverToPassengers';
|
||||
final mId = MarkerId(markerId);
|
||||
final existingMarker = markers.cast<Marker?>().firstWhere(
|
||||
(m) => m?.markerId == mId,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (existingMarker != null) {
|
||||
_smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon);
|
||||
} else {
|
||||
markers = {
|
||||
...markers,
|
||||
Marker(
|
||||
markerId: mId,
|
||||
position: newPosition,
|
||||
rotation: newHeading,
|
||||
icon: InlqBitmap.fromStyleImage(icon),
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
),
|
||||
};
|
||||
update();
|
||||
}
|
||||
mapController?.animateCamera(CameraUpdate.newLatLng(newPosition));
|
||||
}
|
||||
|
||||
void updateMarkerPosition(
|
||||
LatLng newPosition, double newHeading, String icon) {
|
||||
_updateMarkerPosition(newPosition, newHeading, icon);
|
||||
}
|
||||
|
||||
void _smoothlyUpdateMarker(
|
||||
Marker oldMarker, LatLng newPosition, double newHeading, String icon) {
|
||||
double distance = Geolocator.distanceBetween(
|
||||
oldMarker.position.latitude,
|
||||
oldMarker.position.longitude,
|
||||
newPosition.latitude,
|
||||
newPosition.longitude);
|
||||
|
||||
if (distance < 2.0) return;
|
||||
|
||||
final MarkerId markerIdKey = oldMarker.markerId;
|
||||
_animationTimers[markerIdKey.value]?.cancel();
|
||||
|
||||
int ticks = 0;
|
||||
const int totalSteps = 20;
|
||||
const int stepDuration = 50;
|
||||
|
||||
double latStep =
|
||||
(newPosition.latitude - oldMarker.position.latitude) / totalSteps;
|
||||
double lngStep =
|
||||
(newPosition.longitude - oldMarker.position.longitude) / totalSteps;
|
||||
double headingStep = (newHeading - oldMarker.rotation) / totalSteps;
|
||||
|
||||
LatLng currentPos = oldMarker.position;
|
||||
double currentHeading = oldMarker.rotation;
|
||||
|
||||
_animationTimers[markerIdKey.value] =
|
||||
Timer.periodic(const Duration(milliseconds: stepDuration), (timer) {
|
||||
ticks++;
|
||||
|
||||
currentPos =
|
||||
LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep);
|
||||
currentHeading += headingStep;
|
||||
|
||||
final updatedMarker = oldMarker.copyWith(
|
||||
position: currentPos,
|
||||
rotation: currentHeading,
|
||||
icon: InlqBitmap.fromStyleImage(icon),
|
||||
);
|
||||
|
||||
markers = {
|
||||
...markers.where((m) => m.markerId != markerIdKey),
|
||||
updatedMarker,
|
||||
};
|
||||
|
||||
if (mapController != null) {
|
||||
mapController!.animateCamera(CameraUpdate.newLatLng(currentPos));
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
if (ticks >= totalSteps) {
|
||||
timer.cancel();
|
||||
_animationTimers.remove(markerIdKey.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// تحديث موقع العلامة (Marker) واتجاهها بسلاسة على الخريطة.
|
||||
// تحسب الدالة المسافة بين الموقع الحالي والجديد؛ وإذا كانت أكبر من مترين،
|
||||
// تقوم بتقسيم الحركة والدوران إلى 20 خطوة متباعدة بـ 50 مللي ثانية (إجمالي ثانية واحدة).
|
||||
// يتم تحديث موضع العلامة وتحريك الكاميرا تدريجياً لتبدو حركة السيارة انسيابية.
|
||||
void smoothlyUpdateMarker(
|
||||
Marker oldMarker, LatLng newPosition, double newHeading, String icon) {
|
||||
_smoothlyUpdateMarker(oldMarker, newPosition, newHeading, icon);
|
||||
}
|
||||
|
||||
void changeBottomSheetShown({bool? forceValue}) {
|
||||
if (forceValue != null) {
|
||||
isBottomSheetShown = forceValue;
|
||||
} else {
|
||||
isBottomSheetShown = !isBottomSheetShown;
|
||||
}
|
||||
heightBottomSheetShown = isBottomSheetShown == true ? 250 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeCashConfirmPageShown() {
|
||||
isCashConfirmPageShown = !isCashConfirmPageShown;
|
||||
final rideLife = Get.find<RideLifecycleController>();
|
||||
rideLife.isCashSelectedBeforeConfirmRide = true;
|
||||
cashConfirmPageShown = isCashConfirmPageShown == true ? 250 : 0;
|
||||
update();
|
||||
rideLife.update();
|
||||
}
|
||||
|
||||
void changePaymentMethodPageShown() {
|
||||
isPaymentMethodPageShown = !isPaymentMethodPageShown;
|
||||
paymentPageShown = isPaymentMethodPageShown == true ? Get.height * .6 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeMapType() {
|
||||
mapType = !mapType;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeMapTraffic() {
|
||||
mapTrafficON = !mapTrafficON;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeisAnotherOreder(bool val) {
|
||||
isAnotherOreder = val;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeIsWhatsAppOrder(bool val) {
|
||||
isWhatsAppOrder = val;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeCancelRidePageShow() {
|
||||
showCancelRideBottomSheet();
|
||||
isCancelRidePageShown = !isCancelRidePageShown;
|
||||
update();
|
||||
if (Get.isRegistered<RideLifecycleController>()) {
|
||||
Get.find<RideLifecycleController>().update();
|
||||
}
|
||||
}
|
||||
|
||||
void getDrawerMenu() {
|
||||
heightMenuBool = !heightMenuBool;
|
||||
widthMapTypeAndTraffic = heightMenuBool == true ? 0 : 50;
|
||||
heightMenu = heightMenuBool == true ? 80 : 0;
|
||||
widthMenu = heightMenuBool == true ? 110 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeMainBottomMenuMap() {
|
||||
if (isWayPointStopsSheetUtilGetMap == true) {
|
||||
changeWayPointSheet();
|
||||
} else {
|
||||
isMainBottomMenuMap = !isMainBottomMenuMap;
|
||||
mainBottomMenuMapHeight =
|
||||
isMainBottomMenuMap == true ? Get.height * .22 : Get.height * .6;
|
||||
isWayPointSheet = false;
|
||||
if (heightMenuBool == true) {
|
||||
getDrawerMenu();
|
||||
}
|
||||
Get.find<RideLifecycleController>().initilizeGetStorage();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void downPoints() {
|
||||
if (Get.find<WayPointController>().wayPoints.length < 2) {
|
||||
isWayPointStopsSheetUtilGetMap = false;
|
||||
isWayPointSheet = false;
|
||||
wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0;
|
||||
update();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void changeWayPointSheet() {
|
||||
isWayPointSheet = !isWayPointSheet;
|
||||
wayPointSheetHeight = isWayPointSheet == false ? 0 : Get.height * .45;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeWayPointStopsSheet() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPointIndex > -1) {
|
||||
isWayPointStopsSheet = true;
|
||||
isWayPointStopsSheetUtilGetMap = true;
|
||||
}
|
||||
isWayPointStopsSheet = !isWayPointStopsSheet;
|
||||
wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeHeightPlaces() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.placesDestination.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightStartPlaces() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.placesStart.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlacesAll(int index) {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.placeListResponseAll[index].isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlaces1() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPoint1.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlaces2() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPoint2.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlaces3() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPoint3.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlaces4() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPoint4.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void hidePlaces() {
|
||||
height = 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changePickerShown() {
|
||||
isPickerShown = !isPickerShown;
|
||||
heightPickerContainer = isPickerShown == true ? 150 : 90;
|
||||
update();
|
||||
}
|
||||
|
||||
void _initializePolygons() {
|
||||
List<List<LatLng>> universityPolygons =
|
||||
UniversitiesPolygons.universityPolygons;
|
||||
|
||||
for (int i = 0; i < universityPolygons.length; i++) {
|
||||
Polygon polygon = Polygon(
|
||||
polygonId: PolygonId('univ_$i'),
|
||||
points: universityPolygons[i],
|
||||
fillColor: Colors.blueAccent.withOpacity(0.2),
|
||||
strokeColor: Colors.blueAccent,
|
||||
strokeWidth: 2,
|
||||
);
|
||||
polygons.add(polygon);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void _applyLowEndModeIfNeeded() {
|
||||
// Placeholder comment from original
|
||||
}
|
||||
|
||||
Future<void> _initMinimalIcons() async {
|
||||
// Icons are loaded dynamically
|
||||
}
|
||||
|
||||
Future<void> _playRouteAnimation(
|
||||
List<LatLng> coords, LatLngBounds? bounds) async {
|
||||
const List<Color> segmentColors = [
|
||||
Color(0xFF109642), // Green
|
||||
Color(0xFFF59E0B), // Amber
|
||||
Color(0xFF7C3AED), // Purple
|
||||
Color(0xFFEF4444), // Red
|
||||
];
|
||||
|
||||
Set<Polyline> newPolylines = {};
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (locSearch.activeMenuWaypointCount > 0) {
|
||||
List<int> splitIndices = [];
|
||||
for (int w = 0; w < locSearch.activeMenuWaypointCount; w++) {
|
||||
final wp = locSearch.menuWaypoints[w];
|
||||
if (wp == null) continue;
|
||||
int bestIdx = 0;
|
||||
double bestDist = double.infinity;
|
||||
for (int j = 0; j < coords.length; j++) {
|
||||
final dx = coords[j].latitude - wp.latitude;
|
||||
final dy = coords[j].longitude - wp.longitude;
|
||||
final d = dx * dx + dy * dy;
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
bestIdx = j;
|
||||
}
|
||||
}
|
||||
splitIndices.add(bestIdx);
|
||||
}
|
||||
splitIndices.sort();
|
||||
|
||||
List<int> boundaries = [0, ...splitIndices, coords.length - 1];
|
||||
for (int s = 0; s < boundaries.length - 1; s++) {
|
||||
int from = boundaries[s];
|
||||
int to = boundaries[s + 1] + 1;
|
||||
if (to > coords.length) to = coords.length;
|
||||
if (from >= to - 1) continue;
|
||||
final segCoords = coords.sublist(from, to);
|
||||
if (segCoords.length < 2) continue;
|
||||
final color = segmentColors[s % segmentColors.length];
|
||||
|
||||
newPolylines.add(Polyline(
|
||||
polylineId: PolylineId('segment_$s'),
|
||||
points: segCoords,
|
||||
color: color,
|
||||
width: 6,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
newPolylines.add(Polyline(
|
||||
polylineId: const PolylineId('route_primary'),
|
||||
points: coords,
|
||||
color: AppColor.primaryColor,
|
||||
width: 6,
|
||||
));
|
||||
}
|
||||
|
||||
polyLines = newPolylines;
|
||||
update();
|
||||
|
||||
Log.print(
|
||||
'🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map');
|
||||
|
||||
update();
|
||||
|
||||
if (bounds != null) {
|
||||
await _safeAnimateCameraBounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
isPickerShown = false;
|
||||
isPointsPageForRider = false;
|
||||
isBottomSheetShown = false;
|
||||
isCancelRidePageShown = false;
|
||||
isCashConfirmPageShown = false;
|
||||
isPaymentMethodPageShown = false;
|
||||
isRideFinished = false;
|
||||
rideConfirm = false;
|
||||
isMainBottomMenuMap = true;
|
||||
|
||||
isWayPointSheet = false;
|
||||
isWayPointStopsSheet = false;
|
||||
isWayPointStopsSheetUtilGetMap = false;
|
||||
|
||||
heightBottomSheetShown = 0;
|
||||
mainBottomMenuMapHeight = Get.height * 0.22;
|
||||
wayPointSheetHeight = 0;
|
||||
|
||||
markers.clear();
|
||||
polyLines.clear();
|
||||
polylineCoordinates.clear();
|
||||
|
||||
_animationTimers.forEach((key, timer) => timer.cancel());
|
||||
_animationTimers.clear();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_animationTimers.forEach((key, timer) => timer.cancel());
|
||||
_animationTimers.clear();
|
||||
mapController = null;
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
25
siro_rider/lib/controller/home/map/map_screen_binding.dart
Normal file
25
siro_rider/lib/controller/home/map/map_screen_binding.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'map_socket_controller.dart';
|
||||
import 'map_engine_controller.dart';
|
||||
import 'location_search_controller.dart';
|
||||
import 'nearby_drivers_controller.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import 'ui_interactions_controller.dart';
|
||||
|
||||
class MapScreenBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// 1. WebSocket Controller: Permanent and immediate
|
||||
Get.put(MapSocketController());
|
||||
|
||||
// 2. Core Controllers (initialized when the screen opens or on demand)
|
||||
Get.lazyPut(() => MapEngineController());
|
||||
Get.lazyPut(() => LocationSearchController());
|
||||
Get.lazyPut(() => NearbyDriversController());
|
||||
|
||||
// 3. Lifecycle and UI Interaction Controllers
|
||||
Get.lazyPut(() => RideLifecycleController());
|
||||
Get.lazyPut(() => UiInteractionsController(), fenix: true);
|
||||
}
|
||||
}
|
||||
326
siro_rider/lib/controller/home/map/map_socket_controller.dart
Normal file
326
siro_rider/lib/controller/home/map/map_socket_controller.dart
Normal file
@@ -0,0 +1,326 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart' as io_client;
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../main.dart'; // contains global 'box'
|
||||
import '../../../print.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import 'nearby_drivers_controller.dart';
|
||||
import 'map_engine_controller.dart';
|
||||
|
||||
class MapSocketController extends GetxController {
|
||||
late io_client.Socket socket;
|
||||
bool isSocketConnected = false;
|
||||
bool _isSocketInitialized = false;
|
||||
Timer? _heartbeatTimer;
|
||||
DateTime? _lastSocketLocationTime;
|
||||
int _socketLocationUpdatesCount = 0;
|
||||
Timer? _watchdogTimer;
|
||||
|
||||
DateTime? get lastDriverLocationTime => _lastSocketLocationTime;
|
||||
int get socketLocationUpdatesCount => _socketLocationUpdatesCount;
|
||||
|
||||
void initConnectionWithSocket() {
|
||||
if (isSocketConnected) return;
|
||||
|
||||
String passengerId = box.read(BoxName.passengerID).toString();
|
||||
Log.print("🔌 Initializing Socket for Passenger: $passengerId");
|
||||
|
||||
socket = io_client.io(
|
||||
AppLink.serverSocket,
|
||||
io_client.OptionBuilder()
|
||||
.setTransports(['websocket'])
|
||||
.disableAutoConnect()
|
||||
.setQuery({'id': passengerId})
|
||||
.setReconnectionAttempts(20)
|
||||
.setReconnectionDelay(2000)
|
||||
.setReconnectionDelayMax(10000)
|
||||
.enableReconnection()
|
||||
.setTimeout(20000)
|
||||
.setExtraHeaders({'Connection': 'Upgrade'})
|
||||
.build(),
|
||||
);
|
||||
_isSocketInitialized = true;
|
||||
|
||||
socket.connect();
|
||||
|
||||
socket.onConnect((_) {
|
||||
Log.print("✅ Socket Connected Successfully");
|
||||
isSocketConnected = true;
|
||||
_startHeartbeat();
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) {
|
||||
socket.emit('subscribe_driver_location', {
|
||||
'ride_id': rideLifecycle.rideId,
|
||||
'driver_id': rideLifecycle.driverId,
|
||||
});
|
||||
Log.print("📡 Re-subscribed to driver location after connect");
|
||||
}
|
||||
update();
|
||||
});
|
||||
|
||||
socket.onDisconnect((_) {
|
||||
Log.print("⚠️ Socket Disconnected — Auto-Reconnect will handle it");
|
||||
isSocketConnected = false;
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.isActiveRideState()) {
|
||||
Log.print("🔄 Enabling Fast Polling Fallback (4s) until reconnect...");
|
||||
rideLifecycle.startMasterTimerWithInterval(4);
|
||||
}
|
||||
update();
|
||||
});
|
||||
|
||||
socket.onReconnect((_) {
|
||||
Log.print("🔁 Socket Reconnected Successfully!");
|
||||
isSocketConnected = true;
|
||||
_startHeartbeat();
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) {
|
||||
socket.emit('subscribe_driver_location', {
|
||||
'ride_id': rideLifecycle.rideId,
|
||||
'driver_id': rideLifecycle.driverId,
|
||||
});
|
||||
Log.print("📡 Re-subscribed to driver location after reconnect");
|
||||
}
|
||||
|
||||
if (rideLifecycle.isActiveRideState()) {
|
||||
Log.print("✅ Socket back online — stopping Fast Polling Fallback");
|
||||
rideLifecycle.cancelMasterTimer();
|
||||
}
|
||||
update();
|
||||
});
|
||||
|
||||
socket.onReconnectAttempt((attemptNumber) {
|
||||
Log.print("🔄 Socket Reconnect Attempt #$attemptNumber...");
|
||||
});
|
||||
|
||||
socket.onError((error) {
|
||||
Log.print("❌ Socket Error: $error");
|
||||
isSocketConnected = false;
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) {
|
||||
Log.print("❌ Socket Connect Error: $error");
|
||||
isSocketConnected = false;
|
||||
// في الإصدار 1.0.2 أحياناً auto-reconnect لا يعمل بعد connect_error
|
||||
// نتأكد يدوياً من إعادة الاتصال
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
if (!isSocketConnected && _isSocketInitialized) {
|
||||
Log.print("🔄 Manual reconnect after connect_error...");
|
||||
try {
|
||||
socket.connect();
|
||||
} catch (e) {
|
||||
Log.print("Manual reconnect error: $e");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('ride_status_change', (data) {
|
||||
Log.print("📩 Socket Event: ride_status_change -> $data");
|
||||
_handleRideStatusChangeWithSocket(data);
|
||||
});
|
||||
|
||||
socket.on('driver_location_update', (data) {
|
||||
handleDriverLocationUpdate(data);
|
||||
});
|
||||
}
|
||||
|
||||
void _startHeartbeat() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: 15), (timer) {
|
||||
if (isSocketConnected && socket.connected) {
|
||||
socket.emit('heartbeat',
|
||||
{'passenger_id': box.read(BoxName.passengerID).toString()});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool isSocketHealthy() {
|
||||
if (!isSocketConnected) return false;
|
||||
if (_lastSocketLocationTime == null) return false;
|
||||
final diff = DateTime.now().difference(_lastSocketLocationTime!).inSeconds;
|
||||
return diff < 20;
|
||||
}
|
||||
|
||||
void _handleRideStatusChangeWithSocket(dynamic data) {
|
||||
if (data == null || data['status'] == null) return;
|
||||
|
||||
String newStatus = data['status'].toString().toLowerCase();
|
||||
Log.print("🔔 Socket Status Update: $newStatus");
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
|
||||
Map<String, dynamic>? driverInfo;
|
||||
if (data['driver_info'] != null && data['driver_info'] is Map) {
|
||||
driverInfo = Map<String, dynamic>.from(data['driver_info']);
|
||||
}
|
||||
|
||||
switch (newStatus) {
|
||||
case 'accepted':
|
||||
case 'apply':
|
||||
case 'applied':
|
||||
rideLifecycle.processRideAcceptance(
|
||||
driverData: driverInfo, source: "Socket");
|
||||
break;
|
||||
|
||||
case 'arrived':
|
||||
rideLifecycle.processDriverArrival("Socket");
|
||||
break;
|
||||
|
||||
case 'started':
|
||||
case 'begin':
|
||||
rideLifecycle.processRideBegin(source: "Socket");
|
||||
break;
|
||||
|
||||
case 'finished':
|
||||
case 'ended':
|
||||
_onRideFinishedWithSocket(data);
|
||||
break;
|
||||
|
||||
case 'cancelled':
|
||||
rideLifecycle.processRideCancelledByDriver(data, source: "Socket");
|
||||
break;
|
||||
|
||||
case 'no_drivers_found':
|
||||
rideLifecycle.showNoDriverDialog();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _onRideFinishedWithSocket(dynamic data) {
|
||||
Log.print("🏁 Ride Finished (Socket)");
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
|
||||
var rawList = data['DriverList'];
|
||||
List<dynamic> listToSend = [];
|
||||
|
||||
if (rawList != null) {
|
||||
if (rawList is List) {
|
||||
listToSend = rawList;
|
||||
} else if (rawList is String) {
|
||||
try {
|
||||
listToSend = jsonDecode(rawList);
|
||||
} catch (e) {
|
||||
Log.print("Error decoding DriverList: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (listToSend.isEmpty && data['price'] != null) {
|
||||
listToSend = [
|
||||
rideLifecycle.driverId,
|
||||
rideLifecycle.rideId,
|
||||
rideLifecycle.driverToken,
|
||||
data['price'].toString()
|
||||
];
|
||||
}
|
||||
|
||||
rideLifecycle.processRideFinished(listToSend, source: "Socket");
|
||||
}
|
||||
|
||||
void handleDriverLocationUpdate(dynamic data) {
|
||||
if (!isSocketConnected || data == null) return;
|
||||
_lastSocketLocationTime = DateTime.now();
|
||||
_socketLocationUpdatesCount++;
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.driverId.isEmpty &&
|
||||
(data['driver_id'] ?? data['driverId']) != null) {
|
||||
rideLifecycle.driverId =
|
||||
(data['driver_id'] ?? data['driverId']).toString();
|
||||
}
|
||||
|
||||
if (_socketLocationUpdatesCount >= 3 &&
|
||||
rideLifecycle.locationPollingTimer != null) {
|
||||
Log.print("✅ Socket delivering locations reliably. Stopping polling.");
|
||||
rideLifecycle.stopDriverLocationPolling();
|
||||
}
|
||||
|
||||
try {
|
||||
double lat = double.tryParse(
|
||||
(data['latitude'] ?? data['lat'])?.toString() ?? '0') ??
|
||||
0;
|
||||
double lng = double.tryParse(
|
||||
(data['longitude'] ?? data['lng'])?.toString() ?? '0') ??
|
||||
0;
|
||||
double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0;
|
||||
|
||||
if (lat == 0 || lng == 0) return;
|
||||
|
||||
LatLng newPos = LatLng(lat, lng);
|
||||
|
||||
final nearbyDrivers = Get.find<NearbyDriversController>();
|
||||
if (nearbyDrivers.driverCarsLocationToPassengerAfterApplied.isEmpty) {
|
||||
nearbyDrivers.driverCarsLocationToPassengerAfterApplied.add(newPos);
|
||||
} else {
|
||||
nearbyDrivers.driverCarsLocationToPassengerAfterApplied[0] = newPos;
|
||||
}
|
||||
|
||||
double speed = double.tryParse(data['speed']?.toString() ?? '0') ?? 0;
|
||||
rideLifecycle.checkAndRecalculateIfDeviated(
|
||||
newPos,
|
||||
heading: heading,
|
||||
speed: speed,
|
||||
);
|
||||
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
if (mapEngine.mapController != null) {
|
||||
double zoom = 16.5;
|
||||
if (speed > 0) {
|
||||
zoom = 17.0 - ((speed - 10) / 70) * 2.5;
|
||||
zoom = zoom.clamp(14.5, 17.0);
|
||||
}
|
||||
mapEngine.mapController!
|
||||
.animateCamera(CameraUpdate.newLatLngZoom(newPos, zoom));
|
||||
}
|
||||
|
||||
final dynamic distanceValue =
|
||||
data['distance_m'] ?? data['distance_meters'] ?? data['distance'];
|
||||
final double? distanceMeters =
|
||||
double.tryParse(distanceValue?.toString() ?? '');
|
||||
final int? etaSeconds = data['eta_seconds'] == null
|
||||
? null
|
||||
: int.tryParse(data['eta_seconds'].toString());
|
||||
final bool hasServerMetrics = (etaSeconds != null && etaSeconds > 0) ||
|
||||
(distanceMeters != null && distanceMeters > 0);
|
||||
if (hasServerMetrics) {
|
||||
rideLifecycle.updateDriverRouteMetrics(
|
||||
etaSeconds: etaSeconds != null && etaSeconds > 0 ? etaSeconds : null,
|
||||
distanceMeters: distanceMeters,
|
||||
);
|
||||
}
|
||||
|
||||
rideLifecycle.updateDriverMarker(newPos, heading);
|
||||
rideLifecycle.updateRemainingRoute(newPos, updateEta: !hasServerMetrics);
|
||||
rideLifecycle.update();
|
||||
} catch (e) {
|
||||
Log.print('Error in handleDriverLocationUpdate: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void disposeRideSocket() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_watchdogTimer?.cancel();
|
||||
if (_isSocketInitialized) {
|
||||
socket.disconnect();
|
||||
socket.dispose();
|
||||
isSocketConnected = false;
|
||||
_isSocketInitialized = false;
|
||||
Log.print("🔌 Socket Disposed");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
disposeRideSocket();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' show Random, atan2, cos, pi, pow, sin, sqrt;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/api_key.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import 'map_engine_controller.dart';
|
||||
import 'location_search_controller.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import '../../../models/model/locations.dart';
|
||||
import 'car_location.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
|
||||
class NearbyDriversController extends GetxController {
|
||||
List carsLocationByPassenger = [];
|
||||
List<LatLng> driverCarsLocationToPassengerAfterApplied = [];
|
||||
List<CarLocationModel> carLocationsModels = [];
|
||||
String? currentDriverMarkerId;
|
||||
bool lowPerf = false;
|
||||
|
||||
dynamic dataCarsLocationByPassenger;
|
||||
bool noCarString = false;
|
||||
final double minMovementThreshold = 2.0;
|
||||
final Map<String, Timer> _animationTimers = {};
|
||||
|
||||
final List<Map<String, dynamic>> fakeCarData = [];
|
||||
|
||||
Future<bool> getCarsLocationByPassengerAndReloadMarker() async {
|
||||
carsLocationByPassenger = [];
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (locSearch.passengerLocation.latitude == 0 && locSearch.passengerLocation.longitude == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getCarsLocationByPassenger,
|
||||
payload: {
|
||||
'lat': locSearch.passengerLocation.latitude.toString(),
|
||||
'lng': locSearch.passengerLocation.longitude.toString(),
|
||||
'radius': '5',
|
||||
'limit': '50',
|
||||
},
|
||||
);
|
||||
|
||||
if (res == 'failure') {
|
||||
noCarString = true;
|
||||
dataCarsLocationByPassenger = 'failure';
|
||||
update();
|
||||
return false;
|
||||
}
|
||||
|
||||
noCarString = false;
|
||||
var responseData = jsonDecode(res);
|
||||
dataCarsLocationByPassenger = responseData;
|
||||
|
||||
List driversList = [];
|
||||
if (responseData['status'] == true && responseData['data'] != null) {
|
||||
driversList = responseData['data'];
|
||||
} else if (responseData['message'] != null) {
|
||||
driversList = responseData['message'];
|
||||
}
|
||||
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
|
||||
if (driversList.isEmpty) {
|
||||
carsLocationByPassenger.clear();
|
||||
mapEngine.update();
|
||||
return false;
|
||||
}
|
||||
|
||||
carsLocationByPassenger.clear();
|
||||
|
||||
for (var i = 0; i < driversList.length; i++) {
|
||||
var carData = driversList[i];
|
||||
|
||||
double lat = double.tryParse(carData['latitude'].toString()) ?? 0.0;
|
||||
double lng = double.tryParse(carData['longitude'].toString()) ?? 0.0;
|
||||
double heading = double.tryParse(carData['heading'].toString()) ?? 0.0;
|
||||
|
||||
if (lat == 0.0 || lng == 0.0) continue;
|
||||
|
||||
String driverId = (carData['driver_id'] ?? carData['id'] ?? '').toString();
|
||||
if (driverId.isEmpty || driverId == 'null') continue;
|
||||
|
||||
_updateOrCreateMarker(
|
||||
driverId,
|
||||
LatLng(lat, lng),
|
||||
heading,
|
||||
_getIconForCar(carData),
|
||||
);
|
||||
}
|
||||
|
||||
mapEngine.update();
|
||||
return true;
|
||||
}
|
||||
|
||||
void _addFakeCarMarkers(LatLng center, int count) {
|
||||
if (fakeCarData.isEmpty) {
|
||||
Random random = Random();
|
||||
double radiusInKm = 2.5;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
double angle = random.nextDouble() * 2 * pi;
|
||||
double distance = sqrt(random.nextDouble()) * radiusInKm;
|
||||
|
||||
double latOffset = (distance / 111.32);
|
||||
double lonOffset =
|
||||
(distance / (111.32 * cos(center.latitude * pi / 180.0)));
|
||||
|
||||
double lat = center.latitude + (latOffset * cos(angle));
|
||||
double lon = center.longitude + (lonOffset * sin(angle));
|
||||
|
||||
double heading = random.nextDouble() * 360;
|
||||
|
||||
fakeCarData.add({
|
||||
'id': 'fake_$i',
|
||||
'latitude': lat,
|
||||
'longitude': lon,
|
||||
'heading': heading,
|
||||
'gender': 'Male',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (var carData in fakeCarData) {
|
||||
_updateOrCreateMarker(
|
||||
carData['id'].toString(),
|
||||
LatLng(carData['latitude'], carData['longitude']),
|
||||
carData['heading'],
|
||||
_getIconForCar(carData),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void addFakeCarMarkers(LatLng center, int count) {
|
||||
_addFakeCarMarkers(center, count);
|
||||
}
|
||||
|
||||
Future<CarLocation?> getNearestDriverByPassengerLocation() async {
|
||||
final rideLife = Get.find<RideLifecycleController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (!rideLife.rideConfirm) {
|
||||
if (dataCarsLocationByPassenger != 'failure' &&
|
||||
dataCarsLocationByPassenger != null &&
|
||||
dataCarsLocationByPassenger.containsKey('message') &&
|
||||
dataCarsLocationByPassenger['message'] != null &&
|
||||
dataCarsLocationByPassenger['message'].length > 0) {
|
||||
double nearestDistance = double.infinity;
|
||||
CarLocation? nearestCar;
|
||||
|
||||
for (var i = 0;
|
||||
i < dataCarsLocationByPassenger['message'].length;
|
||||
i++) {
|
||||
var carLocation = dataCarsLocationByPassenger['message'][i];
|
||||
|
||||
try {
|
||||
final distance = Geolocator.distanceBetween(
|
||||
locSearch.passengerLocation.latitude,
|
||||
locSearch.passengerLocation.longitude,
|
||||
double.parse(carLocation['latitude']),
|
||||
double.parse(carLocation['longitude']),
|
||||
);
|
||||
|
||||
int durationToPassenger = (distance / 1000 / 25 * 3600).round();
|
||||
update();
|
||||
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance;
|
||||
|
||||
nearestCar = CarLocation(
|
||||
distance: distance,
|
||||
duration: durationToPassenger.toDouble(),
|
||||
id: carLocation['driver_id'],
|
||||
latitude: double.parse(carLocation['latitude']),
|
||||
longitude: double.parse(carLocation['longitude']),
|
||||
);
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('Error calculating distance/duration: $e');
|
||||
}
|
||||
}
|
||||
return nearestCar;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<CarLocation?> getNearestDriverByPassengerLocationAPIGOOGLE() async {
|
||||
final rideLife = Get.find<RideLifecycleController>();
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (mapEngine.polyLines.isEmpty || rideLife.totalCostPassenger == 0) {
|
||||
return null;
|
||||
}
|
||||
if (!rideLife.rideConfirm) {
|
||||
double nearestDistance = double.infinity;
|
||||
if (dataCarsLocationByPassenger != 'failure' &&
|
||||
dataCarsLocationByPassenger != null &&
|
||||
dataCarsLocationByPassenger.containsKey('message') &&
|
||||
dataCarsLocationByPassenger['message'] != null) {
|
||||
if (dataCarsLocationByPassenger['message'].length > 0) {
|
||||
CarLocation? nearestCar;
|
||||
for (var i = 0;
|
||||
i < dataCarsLocationByPassenger['message'].length;
|
||||
i++) {
|
||||
var carLocation = dataCarsLocationByPassenger['message'][i];
|
||||
|
||||
update();
|
||||
String apiUrl =
|
||||
'${AppLink.googleMapsLink}distancematrix/json?destinations=${carLocation['latitude']},${carLocation['longitude']}&origins=${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}&units=metric&key=${AK.mapAPIKEY}';
|
||||
var response = await CRUD().getGoogleApi(link: apiUrl, payload: {});
|
||||
if (response != null && response['status'] == "OK") {
|
||||
var data = response;
|
||||
int distance1 =
|
||||
data['rows'][0]['elements'][0]['distance']['value'];
|
||||
rideLife.distanceByPassenger =
|
||||
data['rows'][0]['elements'][0]['distance']['text'];
|
||||
rideLife.durationToPassenger =
|
||||
data['rows'][0]['elements'][0]['duration']['value'];
|
||||
|
||||
Duration durationFromDriverToPassenger =
|
||||
Duration(seconds: rideLife.durationToPassenger.toInt());
|
||||
rideLife.stringRemainingTimeToPassenger =
|
||||
data['rows'][0]['elements'][0]['duration']['text'];
|
||||
update();
|
||||
if (distance1 < nearestDistance) {
|
||||
nearestDistance = distance1.toDouble();
|
||||
|
||||
nearestCar = CarLocation(
|
||||
distance: distance1.toDouble(),
|
||||
duration: rideLife.durationToPassenger.toDouble(),
|
||||
id: carLocation['driver_id'],
|
||||
latitude: double.parse(carLocation['latitude']),
|
||||
longitude: double.parse(carLocation['longitude']),
|
||||
);
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
Log.print('${response?['status']}: error Google distance matrix');
|
||||
}
|
||||
}
|
||||
return nearestCar;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future getCarForFirstConfirm(String carType) async {
|
||||
bool foundCars = false;
|
||||
int attempt = 0;
|
||||
|
||||
Timer.periodic(const Duration(seconds: 4), (Timer t) async {
|
||||
foundCars = await getCarsLocationByPassengerAndReloadMarker();
|
||||
Log.print('foundCars: $foundCars');
|
||||
|
||||
if (foundCars) {
|
||||
t.cancel();
|
||||
} else if (attempt >= 4) {
|
||||
t.cancel();
|
||||
if (!foundCars) {
|
||||
noCarString = true;
|
||||
dataCarsLocationByPassenger = 'failure';
|
||||
}
|
||||
update();
|
||||
}
|
||||
attempt++;
|
||||
});
|
||||
}
|
||||
|
||||
void startCarLocationSearch(String carType) {
|
||||
int searchInterval = 5;
|
||||
Log.print('searchInterval: $searchInterval');
|
||||
int boundIncreaseStep = 2500;
|
||||
Log.print('boundIncreaseStep: $boundIncreaseStep');
|
||||
int maxAttempts = 3;
|
||||
int maxBoundIncreaseStep = 6000;
|
||||
int attempt = 0;
|
||||
Log.print('initial attempt: $attempt');
|
||||
|
||||
Timer.periodic(Duration(seconds: searchInterval), (Timer timer) async {
|
||||
Log.print('Current attempt: $attempt');
|
||||
bool foundCars = false;
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
if (attempt >= maxAttempts) {
|
||||
timer.cancel();
|
||||
if (foundCars == false) {
|
||||
noCarString = true;
|
||||
update();
|
||||
}
|
||||
} else if (mapEngine.reloadStartApp == true) {
|
||||
Log.print('reloadStartApp: ${mapEngine.reloadStartApp}');
|
||||
foundCars = await getCarsLocationByPassengerAndReloadMarker();
|
||||
Log.print('foundCars: $foundCars');
|
||||
|
||||
if (foundCars) {
|
||||
timer.cancel();
|
||||
} else {
|
||||
attempt++;
|
||||
Log.print('Incrementing attempt to: $attempt');
|
||||
|
||||
if (boundIncreaseStep < maxBoundIncreaseStep) {
|
||||
boundIncreaseStep += 1500;
|
||||
if (boundIncreaseStep > maxBoundIncreaseStep) {
|
||||
boundIncreaseStep = maxBoundIncreaseStep;
|
||||
}
|
||||
Log.print('New boundIncreaseStep: $boundIncreaseStep');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String _getIconForCar(Map<String, dynamic> carData) {
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
if (carData['model'].toString().contains('دراجة')) {
|
||||
return mapEngine.motoIcon;
|
||||
} else if (carData['gender'] == 'Female') {
|
||||
return mapEngine.ladyIcon;
|
||||
} else {
|
||||
return mapEngine.carIcon;
|
||||
}
|
||||
}
|
||||
|
||||
void _updateOrCreateMarker(
|
||||
String markerId, LatLng newPosition, double newHeading, String icon) {
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
if (!mapEngine.isIconsLoaded) {
|
||||
Log.print("⚠️ Skipping drawing marker $markerId because map icons are not fully loaded yet.");
|
||||
return;
|
||||
}
|
||||
final mId = MarkerId(markerId);
|
||||
final existingMarker = mapEngine.markers.cast<Marker?>().firstWhere(
|
||||
(m) => m?.markerId == mId,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (existingMarker == null) {
|
||||
mapEngine.markers = {
|
||||
...mapEngine.markers,
|
||||
Marker(
|
||||
markerId: mId,
|
||||
position: newPosition,
|
||||
rotation: newHeading,
|
||||
icon: InlqBitmap.fromStyleImage(icon),
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
),
|
||||
};
|
||||
mapEngine.update();
|
||||
} else {
|
||||
double distance = Geolocator.distanceBetween(
|
||||
existingMarker.position.latitude,
|
||||
existingMarker.position.longitude,
|
||||
newPosition.latitude,
|
||||
newPosition.longitude);
|
||||
if (distance >= minMovementThreshold) {
|
||||
_smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _smoothlyUpdateMarker(
|
||||
Marker oldMarker, LatLng newPosition, double newHeading, String icon) {
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
final MarkerId markerIdKey = oldMarker.markerId;
|
||||
|
||||
_animationTimers[markerIdKey.value]?.cancel();
|
||||
|
||||
int ticks = 0;
|
||||
const int totalSteps = 20;
|
||||
const int stepDuration = 50;
|
||||
|
||||
double latStep =
|
||||
(newPosition.latitude - oldMarker.position.latitude) / totalSteps;
|
||||
double lngStep =
|
||||
(newPosition.longitude - oldMarker.position.longitude) / totalSteps;
|
||||
double headingStep = (newHeading - oldMarker.rotation) / totalSteps;
|
||||
|
||||
LatLng currentPos = oldMarker.position;
|
||||
double currentHeading = oldMarker.rotation;
|
||||
|
||||
_animationTimers[markerIdKey.value] =
|
||||
Timer.periodic(const Duration(milliseconds: stepDuration), (timer) {
|
||||
ticks++;
|
||||
|
||||
currentPos =
|
||||
LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep);
|
||||
currentHeading += headingStep;
|
||||
|
||||
final updatedMarker = oldMarker.copyWith(
|
||||
position: currentPos,
|
||||
rotation: currentHeading,
|
||||
icon: InlqBitmap.fromStyleImage(icon),
|
||||
);
|
||||
|
||||
mapEngine.markers = {
|
||||
...mapEngine.markers.where((m) => m.markerId != markerIdKey),
|
||||
updatedMarker,
|
||||
};
|
||||
|
||||
if (mapEngine.mapController != null) {
|
||||
mapEngine.mapController!.animateCamera(CameraUpdate.newLatLng(currentPos));
|
||||
}
|
||||
|
||||
mapEngine.update();
|
||||
|
||||
if (ticks >= totalSteps) {
|
||||
timer.cancel();
|
||||
_animationTimers.remove(markerIdKey.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
double calculateBearing(double lat1, double lon1, double lat2, double lon2) {
|
||||
double deltaLon = lon2 - lon1;
|
||||
double y = sin(deltaLon) * cos(lat2);
|
||||
double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(deltaLon);
|
||||
double bearing = atan2(y, x);
|
||||
return (bearing * 180 / pi + 360) % 360;
|
||||
}
|
||||
|
||||
void analyzeBehavior(Position currentPosition, List<LatLng> routePoints) {
|
||||
double actualBearing = currentPosition.heading;
|
||||
double expectedBearing = calculateBearing(
|
||||
routePoints[0].latitude,
|
||||
routePoints[0].longitude,
|
||||
routePoints[1].latitude,
|
||||
routePoints[1].longitude,
|
||||
);
|
||||
|
||||
double bearingDifference = (expectedBearing - actualBearing).abs();
|
||||
if (bearingDifference > 30) {
|
||||
Log.print("⚠️ السائق انحرف عن المسار!");
|
||||
}
|
||||
}
|
||||
|
||||
void detectStops(Position currentPosition) {
|
||||
if (currentPosition.speed < 0.5) {
|
||||
Log.print("🚦 السائق توقف في موقع غير متوقع!");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> detectPerfMode() async {
|
||||
try {
|
||||
if (GetPlatform.isAndroid) {
|
||||
final info = await DeviceInfoPlugin().androidInfo;
|
||||
final sdk = info.version.sdkInt;
|
||||
final ram = info.availableRamSize;
|
||||
lowPerf = (sdk < 28) || (ram > 0 && ram < 3 * 1024 * 1024 * 1024);
|
||||
} else {
|
||||
lowPerf = false;
|
||||
}
|
||||
} catch (_) {
|
||||
lowPerf = false;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_animationTimers.forEach((key, timer) => timer.cancel());
|
||||
_animationTimers.clear();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
4558
siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart
Normal file
4558
siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart
Normal file
File diff suppressed because it is too large
Load Diff
10
siro_rider/lib/controller/home/map/ride_state.dart
Normal file
10
siro_rider/lib/controller/home/map/ride_state.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
enum RideState {
|
||||
noRide, // لا يوجد رحلة جارية، عرض واجهة البحث
|
||||
cancelled, // تم إلغاء الرحلة
|
||||
preCheckReview, // يوجد رحلة منتهية، تحقق من التقييم
|
||||
searching, // جاري البحث عن كابتن
|
||||
driverApplied, // تم قبول الطلب
|
||||
driverArrived, // وصل السائق
|
||||
inProgress, // الرحلة بدأت بالفعل
|
||||
finished, // انتهت الرحلة (سيتم تحويلها إلى preCheckReview)
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../main.dart'; // contains global 'box'
|
||||
import '../../../print.dart';
|
||||
import '../../../services/emergency_signal_service.dart';
|
||||
import '../../../views/widgets/elevated_btn.dart';
|
||||
import '../../../views/widgets/mydialoug.dart';
|
||||
import '../../../views/widgets/my_textField.dart';
|
||||
import '../../../views/home/map_page_passenger.dart';
|
||||
import '../../../views/widgets/error_snakbar.dart';
|
||||
import '../../../models/model/painter_copoun.dart';
|
||||
import '../../functions/launch.dart';
|
||||
import '../../firebase/local_notification.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/tts.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import 'location_search_controller.dart';
|
||||
import 'map_engine_controller.dart';
|
||||
|
||||
class UiInteractionsController extends GetxController {
|
||||
TextEditingController sosPhonePassengerProfile = TextEditingController();
|
||||
TextEditingController whatsAppLocationText = TextEditingController();
|
||||
final sosFormKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
EmergencySignalService.instance.startListening(() {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.statusRide == 'Begin' ||
|
||||
rideLifecycle.statusRide == 'start') {
|
||||
Log.print("🚨 Emergency shake verified! Prompting SOS...");
|
||||
sosPassenger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _ensureSosNumber(Function onSuccess) async {
|
||||
String? storedPhone = box.read(BoxName.sosPhonePassenger);
|
||||
if (storedPhone != null && storedPhone.isNotEmpty) {
|
||||
onSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
sosPhonePassengerProfile.clear();
|
||||
Get.defaultDialog(
|
||||
title: 'Add SOS Phone'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Form(
|
||||
key: sosFormKey,
|
||||
child: Column(
|
||||
children: [
|
||||
MyTextForm(
|
||||
controller: sosPhonePassengerProfile,
|
||||
label: 'insert sos phone'.tr,
|
||||
hint: 'e.g. 0912345678 (Default +963)'.tr,
|
||||
type: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Note: If no country code is entered, it will be saved as Syrian (+963).".tr,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Save'.tr,
|
||||
onPressed: () async {
|
||||
if (sosFormKey.currentState!.validate()) {
|
||||
Get.back();
|
||||
var numberPhone =
|
||||
formatSyrianPhoneNumber(sosPhonePassengerProfile.text);
|
||||
|
||||
await CRUD().post(
|
||||
link: AppLink.updateprofile,
|
||||
payload: {
|
||||
'id': box.read(BoxName.passengerID),
|
||||
'sosPhone': numberPhone,
|
||||
},
|
||||
);
|
||||
|
||||
box.write(BoxName.sosPhonePassenger, numberPhone);
|
||||
onSuccess();
|
||||
}
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Cancel'.tr,
|
||||
onPressed: () => Get.back(),
|
||||
kolor: AppColor.redColor,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void sosPassenger() {
|
||||
_ensureSosNumber(() {
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: "Emergency SOS".tr,
|
||||
titleStyle: AppStyle.title.copyWith(color: AppColor.redColor),
|
||||
content: Column(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, size: 50, color: AppColor.redColor),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Do you want to send an emergency message to your SOS contact?".tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: "Send SOS".tr,
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
_shareTripDetailsSOS();
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: "I'm Safe".tr,
|
||||
kolor: AppColor.greenColor,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _shareTripDetailsSOS() {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
String message = "**Emergency SOS from Passenger:**\n";
|
||||
String origin = locSearch.startNameAddress;
|
||||
String destination = locSearch.endNameAddress;
|
||||
|
||||
message += "* ${'Origin'.tr}: $origin\n";
|
||||
message += "* ${'Destination'.tr}: $destination\n";
|
||||
message += "* ${'Driver Name'.tr}: ${rideLifecycle.driverName}\n";
|
||||
message +=
|
||||
"* ${'Car'.tr}: ${rideLifecycle.make} - ${rideLifecycle.model} - ${rideLifecycle.licensePlate}\n";
|
||||
message += "* ${'Phone'.tr}: ${rideLifecycle.driverPhone}\n\n";
|
||||
|
||||
message +=
|
||||
"${'Location'.tr}: https://www.google.com/maps/search/?api=1&query=${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}\n";
|
||||
message += "Please help! Contact me as soon as possible.".tr;
|
||||
|
||||
launchCommunication(
|
||||
'whatsapp', box.read(BoxName.sosPhonePassenger), message);
|
||||
}
|
||||
|
||||
String formatSyrianPhone(String phone) {
|
||||
phone = phone.replaceAll(' ', '').replaceAll('+', '');
|
||||
if (phone.startsWith('00963')) {
|
||||
phone = phone.replaceFirst('00963', '963');
|
||||
}
|
||||
if (phone.startsWith('0963')) {
|
||||
phone = phone.replaceFirst('0963', '963');
|
||||
}
|
||||
if (phone.startsWith('963')) {
|
||||
return phone;
|
||||
}
|
||||
if (phone.startsWith('09')) {
|
||||
return '963' + phone.substring(1);
|
||||
}
|
||||
if (phone.startsWith('9') && phone.length == 9) {
|
||||
return '963' + phone;
|
||||
}
|
||||
return phone;
|
||||
}
|
||||
|
||||
String formatSyrianPhoneNumber(String phoneNumber) {
|
||||
String trimmedPhone = phoneNumber.trim();
|
||||
if (trimmedPhone.startsWith('09')) {
|
||||
return '963${trimmedPhone.substring(1)}';
|
||||
}
|
||||
if (trimmedPhone.startsWith('963')) {
|
||||
return trimmedPhone;
|
||||
}
|
||||
return '963$trimmedPhone';
|
||||
}
|
||||
|
||||
void sendSMS(String to) async {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
String formattedDriverPhone =
|
||||
rideLifecycle.driverPhone.replaceAll(' ', '').replaceAll('+', '');
|
||||
|
||||
String message =
|
||||
'Hi! This is ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n I am using ${box.read(AppInformation.appName)} to ride with ${rideLifecycle.passengerName} as the driver. ${rideLifecycle.passengerName} \nis driving a ${rideLifecycle.model}\n with license plate ${rideLifecycle.licensePlate}.\n I am currently located at ${Get.find<LocationSearchController>().passengerLocation}.\n If you need to reach me, please contact the driver directly at\n\n $formattedDriverPhone.';
|
||||
|
||||
launchCommunication('sms', to, message);
|
||||
}
|
||||
|
||||
void sendWhatsapp(String to) async {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
String formattedPhone = formatSyrianPhone(to);
|
||||
|
||||
String message =
|
||||
'${'${'Hi! This is'.tr} ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n${' I am using'.tr}'} ${AppInformation.appName}${' to ride with'.tr} ${rideLifecycle.passengerName}${' as the driver.'.tr} ${rideLifecycle.passengerName} \n${'is driving a '.tr}${rideLifecycle.model}\n${' with license plate '.tr}${rideLifecycle.licensePlate}.\n${' I am currently located at '.tr} https://www.google.com/maps/place/${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}.\n${' If you need to reach me, please contact the driver directly at'.tr}\n\n ${rideLifecycle.driverPhone}.';
|
||||
|
||||
launchCommunication('whatsapp', formattedPhone, message);
|
||||
}
|
||||
|
||||
Future<dynamic> driverArrivePassengerDialoge() {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
return Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'Hi ,I Arrive your location'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
middleText: 'Please go to Car Driver'.tr,
|
||||
middleTextStyle: AppStyle.title,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok I will go now.'.tr,
|
||||
onPressed: () {
|
||||
NotificationService.sendNotification(
|
||||
target: rideLifecycle.driverToken.toString(),
|
||||
title: 'Hi ,I will go now'.tr,
|
||||
body: 'I will go now'.tr,
|
||||
isTopic: false,
|
||||
tone: 'ding',
|
||||
driverList: [],
|
||||
category: 'Hi ,I will go now',
|
||||
);
|
||||
Get.back();
|
||||
rideLifecycle.remainingTime = 0;
|
||||
rideLifecycle.update();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void getDialog(String title, String? midTitle, VoidCallback onPressed) {
|
||||
final textToSpeechController = Get.find<TextToSpeechController>();
|
||||
Get.defaultDialog(
|
||||
title: title,
|
||||
titleStyle: AppStyle.title,
|
||||
middleTextStyle: AppStyle.title,
|
||||
content: Column(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await textToSpeechController.speakText(title ?? midTitle!);
|
||||
},
|
||||
icon: const Icon(Icons.headphones),
|
||||
),
|
||||
Text(
|
||||
midTitle!,
|
||||
style: AppStyle.title,
|
||||
)
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
onPressed: onPressed,
|
||||
kolor: AppColor.greenColor,
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Cancel',
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future shareTripWithFamily() async {
|
||||
_ensureSosNumber(() {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
String storedPhone = box.read(BoxName.sosPhonePassenger)!;
|
||||
|
||||
if (rideLifecycle.rideId == 'yet' || rideLifecycle.driverId.isEmpty) {
|
||||
Get.snackbar("Alert".tr, "Wait for the trip to start first".tr);
|
||||
return;
|
||||
}
|
||||
|
||||
var numberPhone = formatSyrianPhoneNumber(storedPhone);
|
||||
String trackingLink = rideLifecycle.generateTrackingLink(
|
||||
rideLifecycle.rideId, rideLifecycle.driverId);
|
||||
|
||||
String message = """
|
||||
مرحباً، تابع رحلتي مباشرة على تطبيق انطلق 🚗
|
||||
|
||||
يمكنك تتبع مسار الرحلة من هنا:
|
||||
$trackingLink
|
||||
|
||||
السائق: ${rideLifecycle.passengerName}
|
||||
السيارة: ${rideLifecycle.model} - ${rideLifecycle.licensePlate}
|
||||
شكراً لاستخدامك انطلق!
|
||||
"""
|
||||
.tr;
|
||||
|
||||
String messageEn = """Hello, follow my trip live on Intaleq 🚗
|
||||
|
||||
Track my ride here:
|
||||
$trackingLink
|
||||
|
||||
Driver: ${rideLifecycle.passengerName}
|
||||
Car: ${rideLifecycle.model} - ${rideLifecycle.licensePlate}
|
||||
Thank you for using Intaleq!
|
||||
""";
|
||||
|
||||
String userLanguage = box.read(BoxName.lang) ?? 'ar';
|
||||
message = (userLanguage == 'ar') ? message : messageEn;
|
||||
|
||||
Log.print("Sending WhatsApp to: $numberPhone");
|
||||
launchCommunication('whatsapp', numberPhone, message);
|
||||
|
||||
box.write(BoxName.parentTripSelected, true);
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
Future getTokenForParent() async {
|
||||
_ensureSosNumber(() async {
|
||||
String storedPhone = box.read(BoxName.sosPhonePassenger)!;
|
||||
var numberPhone = formatSyrianPhoneNumber(storedPhone);
|
||||
Log.print("Searching for Parent Token with Phone: $numberPhone");
|
||||
|
||||
var res = await CRUD()
|
||||
.post(link: AppLink.getTokenParent, payload: {'phone': numberPhone});
|
||||
|
||||
if (res is Map<String, dynamic>) {
|
||||
handleResponse(res);
|
||||
} else {
|
||||
try {
|
||||
var decoded = jsonDecode(res);
|
||||
handleResponse(decoded);
|
||||
} catch (e) {
|
||||
Log.print("Error parsing parent response: $res");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void handleResponse(Map<String, dynamic> res) {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (res['status'] == 'failure') {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
|
||||
Get.defaultDialog(
|
||||
title: "No user found".tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Column(
|
||||
children: [
|
||||
Text(
|
||||
"No passenger found for the given phone number".tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Send Intaleq app to him".tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(color: AppColor.greenColor, fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Send Invite'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
var rawPhone = box.read(BoxName.sosPhonePassenger);
|
||||
if (rawPhone == null) return;
|
||||
var phone = formatSyrianPhoneNumber(rawPhone);
|
||||
|
||||
var message = '''Dear Friend,
|
||||
|
||||
🚀 I have just started an exciting trip on Intaleq!
|
||||
Download the app to track my ride:
|
||||
|
||||
👉 Android: https://play.google.com/store/apps/details?id=com.Intaleq.intaleq&hl=en-US
|
||||
👉 iOS: https://apps.apple.com/st/app/intaleq-rider/id6748075179
|
||||
|
||||
See you there!
|
||||
Intaleq Team''';
|
||||
|
||||
launchCommunication('whatsapp', phone, message);
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Cancel'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
);
|
||||
} else if (res['status'] == 'success') {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
|
||||
Get.snackbar("Success".tr, "The invitation was sent successfully".tr,
|
||||
backgroundColor: AppColor.greenColor, colorText: Colors.white);
|
||||
|
||||
List tokensData = res['data'];
|
||||
for (var device in tokensData) {
|
||||
String tokenParent = device['token'];
|
||||
|
||||
NotificationService.sendNotification(
|
||||
category: "Trip Monitoring",
|
||||
target: tokenParent,
|
||||
title: "Trip Monitoring".tr,
|
||||
body: "Click to track the trip".tr,
|
||||
isTopic: false,
|
||||
tone: 'tone1',
|
||||
driverList: [rideLifecycle.rideId, rideLifecycle.driverId],
|
||||
);
|
||||
box.write(BoxName.tokenParent, tokenParent);
|
||||
}
|
||||
box.write(BoxName.parentTripSelected, true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
EmergencySignalService.instance.stopListening();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
7751
siro_rider/lib/controller/home/map_passenger_controller.dart
Normal file
7751
siro_rider/lib/controller/home/map_passenger_controller.dart
Normal file
File diff suppressed because it is too large
Load Diff
14
siro_rider/lib/controller/home/menu_controller.dart
Normal file
14
siro_rider/lib/controller/home/menu_controller.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MyMenuController extends GetxController {
|
||||
bool isDrawerOpen = true;
|
||||
|
||||
void getDrawerMenu() {
|
||||
if (isDrawerOpen == true) {
|
||||
isDrawerOpen = false;
|
||||
} else {
|
||||
isDrawerOpen = true;
|
||||
}
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_rider/constant/box_name.dart';
|
||||
import 'package:siro_rider/constant/links.dart';
|
||||
import 'package:siro_rider/controller/functions/crud.dart';
|
||||
import 'package:siro_rider/main.dart';
|
||||
|
||||
class CaptainWalletController extends GetxController {
|
||||
bool isLoading = false;
|
||||
Map walletDate = {};
|
||||
Map walletDateVisa = {};
|
||||
Map walletDriverPointsDate = {};
|
||||
final formKey = GlobalKey<FormState>();
|
||||
String totalAmount = '0';
|
||||
String totalAmountVisa = '0';
|
||||
String totalPoints = '0';
|
||||
final amountFromBudgetController = TextEditingController();
|
||||
|
||||
payFromBudget() async {
|
||||
if (formKey.currentState!.validate()) {
|
||||
var pointFromBudget = box.read(BoxName.countryCode) == 'Jordan'
|
||||
? int.parse((amountFromBudgetController.text)) * 100
|
||||
: int.parse((amountFromBudgetController.text));
|
||||
|
||||
await addDriverPayment('fromBudgetToPoints',
|
||||
int.parse((amountFromBudgetController.text)) * -1);
|
||||
Future.delayed(const Duration(seconds: 2));
|
||||
await addDriverWallet('fromBudget', pointFromBudget.toString());
|
||||
update();
|
||||
Get.back();
|
||||
// getCaptainWalletFromRide();
|
||||
// getCaptainWalletFromBuyPoints();
|
||||
// checkAccountCaptainBank();
|
||||
}
|
||||
}
|
||||
|
||||
// Future getCaptainWalletFromRide() async {
|
||||
// isLoading = true;
|
||||
// update();
|
||||
// var res = await CRUD().get(
|
||||
// link: AppLink.getAllPaymentFromRide,
|
||||
// payload: {'driverID': box.read(BoxName.driverID)},
|
||||
// );
|
||||
// walletDate = jsonDecode(res);
|
||||
// totalAmount = walletDate['message'][0]['total_amount'].toString() == null
|
||||
// ? '0'
|
||||
// : walletDate['message'][0]['total_amount'];
|
||||
|
||||
// var res1 = await CRUD().get(
|
||||
// link: AppLink.getAllPaymentVisa,
|
||||
// payload: {'driverID': box.read(BoxName.driverID)});
|
||||
// walletDateVisa = jsonDecode(res1);
|
||||
// totalAmountVisa = walletDateVisa['message'][0]['diff'].toString() == null
|
||||
// ? '0'
|
||||
// : walletDateVisa['message'][0]['diff'];
|
||||
// isLoading = false;
|
||||
// update();
|
||||
// }
|
||||
|
||||
// Future getCaptainWalletFromBuyPoints() async {
|
||||
// isLoading = true;
|
||||
// update();
|
||||
// var res = await CRUD().get(
|
||||
// link: AppLink.getDriverPaymentPoints,
|
||||
// payload: {'driverID': box.read(BoxName.driverID)},
|
||||
// );
|
||||
// walletDriverPointsDate = jsonDecode(res);
|
||||
// if (walletDriverPointsDate['message'][0]['driverID'].toString() ==
|
||||
// box.read(BoxName.driverID)) {
|
||||
// double totalPointsDouble = double.parse(
|
||||
// walletDriverPointsDate['message'][0]['total_amount'].toString());
|
||||
// totalPoints = totalPointsDouble.toStringAsFixed(0);
|
||||
// } else {
|
||||
// totalPoints = '0';
|
||||
// }
|
||||
|
||||
// isLoading = false;
|
||||
// update();
|
||||
// }
|
||||
|
||||
late String paymentID;
|
||||
Future addDriverPayment(String paymentMethod, amount) async {
|
||||
var res =
|
||||
await CRUD().postWallet(link: AppLink.addDriverPaymentPoints, payload: {
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
'amount': amount.toString(),
|
||||
'payment_method': paymentMethod.toString(),
|
||||
});
|
||||
var d = jsonDecode(res);
|
||||
paymentID = d['message'].toString();
|
||||
}
|
||||
|
||||
Future addDriverWallet(String paymentMethod, point) async {
|
||||
await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: {
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
'paymentID': paymentID.toString(),
|
||||
'amount': point,
|
||||
'paymentMethod': paymentMethod.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
// getCaptainWalletFromRide();
|
||||
// getCaptainWalletFromBuyPoints();
|
||||
// checkAccountCaptainBank();
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../functions/digit_obsecur_formate.dart';
|
||||
import '../../functions/secure_storage.dart';
|
||||
|
||||
class CreditCardController extends GetxController {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
final TextEditingController cardNumberController = TextEditingController();
|
||||
final TextEditingController cardHolderNameController =
|
||||
TextEditingController();
|
||||
final TextEditingController expiryDateController = TextEditingController();
|
||||
final TextEditingController cvvCodeController = TextEditingController();
|
||||
openPayment() async {
|
||||
String? cardNumber = await SecureStorage().readData(BoxName.cardNumber);
|
||||
String? cardHolderName =
|
||||
await SecureStorage().readData(BoxName.cardHolderName);
|
||||
String? expiryDate = await SecureStorage().readData(BoxName.expiryDate);
|
||||
String? cvvCode = await SecureStorage().readData(BoxName.cvvCode);
|
||||
|
||||
// if (cvvCode != null && cvvCode.isNotEmpty) {
|
||||
// final maskedCardNumber = DigitObscuringFormatter()
|
||||
// .formatEditUpdate(
|
||||
// TextEditingValue.empty,
|
||||
// TextEditingValue(text: cardNumber ?? ''),
|
||||
// )
|
||||
// .text;
|
||||
|
||||
// cardNumberController.text = maskedCardNumber;
|
||||
// cardHolderNameController.text = cardHolderName ?? '';
|
||||
// expiryDateController.text = expiryDate ?? '';
|
||||
// cvvCodeController.text = cvvCode;
|
||||
// }
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
openPayment();
|
||||
// String? cardNumber = await SecureStorage().readData(BoxName.cardNumber);
|
||||
// String? cardHolderName =
|
||||
// await SecureStorage().readData(BoxName.cardHolderName);
|
||||
// String? expiryDate = await SecureStorage().readData(BoxName.expiryDate);
|
||||
// String? cvvCode = await SecureStorage().readData(BoxName.cvvCode);
|
||||
|
||||
// if (cvvCode != null && cvvCode.isNotEmpty) {
|
||||
// final maskedCardNumber = DigitObscuringFormatter()
|
||||
// .formatEditUpdate(
|
||||
// TextEditingValue.empty,
|
||||
// TextEditingValue(text: cardNumber ?? ''),
|
||||
// )
|
||||
// .text;
|
||||
|
||||
// cardNumberController.text = maskedCardNumber;
|
||||
// cardHolderNameController.text = cardHolderName ?? '';
|
||||
// expiryDateController.text = expiryDate ?? '';
|
||||
// cvvCodeController.text = cvvCode;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
class CreditCardModel {
|
||||
String cardNumber;
|
||||
String cardHolderName;
|
||||
String expiryDate;
|
||||
String cvvCode;
|
||||
|
||||
CreditCardModel({
|
||||
required this.cardNumber,
|
||||
required this.cardHolderName,
|
||||
required this.expiryDate,
|
||||
required this.cvvCode,
|
||||
});
|
||||
}
|
||||
155
siro_rider/lib/controller/home/points_for_rider_controller.dart
Normal file
155
siro_rider/lib/controller/home/points_for_rider_controller.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
|
||||
|
||||
import '../../constant/api_key.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../functions/crud.dart';
|
||||
import '../functions/location_controller.dart';
|
||||
|
||||
class PointsForRiderController extends GetxController {
|
||||
List<String> locations = [];
|
||||
String hintTextDestinationPoint = 'Search for your destination'.tr;
|
||||
TextEditingController placeStartController = TextEditingController();
|
||||
|
||||
void addLocation(String location) {
|
||||
locations.add(location);
|
||||
update();
|
||||
}
|
||||
|
||||
void getTextFromList(String location) {
|
||||
locations.add(location);
|
||||
update();
|
||||
Get.back();
|
||||
}
|
||||
|
||||
void removeLocation(int index) {
|
||||
locations.removeAt(index);
|
||||
update();
|
||||
}
|
||||
|
||||
void onReorder(int oldIndex, int newIndex) {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
update();
|
||||
}
|
||||
|
||||
final item = locations.removeAt(oldIndex);
|
||||
locations.insert(newIndex, item);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
class LocationModel {
|
||||
String name;
|
||||
double lat, lon;
|
||||
|
||||
LocationModel({required this.name, required this.lat, required this.lon});
|
||||
}
|
||||
|
||||
class WayPointController extends GetxController {
|
||||
// A list of text editing controllers for each text field
|
||||
// final textFields = [TextEditingController()].obs;
|
||||
List<String> wayPoints = [];
|
||||
List<List<dynamic>> placeListResponse = [];
|
||||
double wayPointHeight = 400;
|
||||
String hintTextDestinationPoint = 'Search for your destination'.tr;
|
||||
TextEditingController textSearchCotroller = TextEditingController();
|
||||
// A list of places corresponding to each text field
|
||||
final places = <String>[];
|
||||
|
||||
final hintTextPointList = <String>[];
|
||||
late LatLng myLocation;
|
||||
|
||||
void addWayPoints() {
|
||||
String wayPoint = 'Add a Stop'.tr;
|
||||
|
||||
if (wayPoints.length < 5) {
|
||||
wayPoints.add(wayPoint);
|
||||
update();
|
||||
} else {
|
||||
Get.defaultDialog(
|
||||
title: 'This is most WayPoints',
|
||||
titleStyle: AppStyle.title,
|
||||
middleText: '');
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void removeTextField(int index) {
|
||||
wayPoints.removeAt(index);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
// A method to reorder the text fields and the places
|
||||
void reorderTextFields(int oldIndex, int newIndex) {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final wayPoint = wayPoints.removeAt(oldIndex);
|
||||
wayPoints.insert(newIndex, wayPoint);
|
||||
update();
|
||||
}
|
||||
|
||||
void updatePlace(int index, String input) async {
|
||||
var url =
|
||||
'${AppLink.googleMapsLink}place/nearbysearch/json?keyword=$input&location=${myLocation.latitude},${myLocation.longitude}&radius=50000&language=en&key=${AK.mapAPIKEY.toString()}';
|
||||
var response = await CRUD().getGoogleApi(link: url, payload: {});
|
||||
// final place = input;
|
||||
// if (index == 0) {
|
||||
List<dynamic> newList = [];
|
||||
placeListResponse.add(newList);
|
||||
newList = response['results'];
|
||||
placeListResponse[index].add(newList);
|
||||
update();
|
||||
// }
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
// Get.put(LocationController());
|
||||
addWayPoints();
|
||||
myLocation = Get.find<RideLifecycleController>().passengerLocation;
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
wayPoints.clear();
|
||||
addWayPoints();
|
||||
placeListResponse.clear();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceList extends StatelessWidget {
|
||||
// Get the controller instance
|
||||
final controller = Get.find<WayPointController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Use the Obx widget to rebuild the widget when the controller changes
|
||||
return Obx(() {
|
||||
// Use the ListView widget to display the list of places
|
||||
return ListView(
|
||||
// The children of the list are the places
|
||||
children: [
|
||||
// Loop through the places in the controller
|
||||
for (final place in controller.places)
|
||||
// Create a text widget for each place
|
||||
Text(
|
||||
// Use the place as the text
|
||||
place,
|
||||
|
||||
// Add some style and padding
|
||||
style: const TextStyle(fontSize: 18.0),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
159
siro_rider/lib/controller/home/precise_comparison_results.txt
Normal file
159
siro_rider/lib/controller/home/precise_comparison_results.txt
Normal file
@@ -0,0 +1,159 @@
|
||||
--- PRECISE MISSING METHODS ---
|
||||
Total original methods/getters/setters: 270
|
||||
Total defined in split controllers: 270
|
||||
Total missing: 53
|
||||
- Column
|
||||
- CupertinoDialogAction
|
||||
- Future
|
||||
- _applyLowEndModeIfNeeded
|
||||
- _buildOsrmWaypointCoords
|
||||
- _calculateDistance
|
||||
- _checkAndRecalculateIfDeviated
|
||||
- _fillDriverDataLocally
|
||||
- _haversineKm
|
||||
- _initMinimalIcons
|
||||
- _initializePolygons
|
||||
- _isActiveRideState
|
||||
- _kmToLatDelta
|
||||
- _kmToLngDelta
|
||||
- _onDriverAcceptedWithSocket
|
||||
- _onDriverArrivedWithSocket
|
||||
- _onRideCancelledWithSocket
|
||||
- _onRideStartedWithSocket
|
||||
- _playRouteAnimation
|
||||
- _relevanceScore
|
||||
- _restorePolyline
|
||||
- _retryProcess
|
||||
- _stageNiceToHave
|
||||
- _stagePricingAndState
|
||||
- _startMasterTimer
|
||||
- _startMasterTimerWithInterval
|
||||
- _startPollingFallback
|
||||
- _stopDriverLocationPolling
|
||||
- _updateDriverMarker
|
||||
- addPostFrameCallback
|
||||
- cancelRide
|
||||
- checkPassengerLocation
|
||||
- currentDriverMarkerId
|
||||
- detectPerfMode
|
||||
- directions
|
||||
- getAIKey
|
||||
- getDirectionMap
|
||||
- getDistanceFromDriverAfterAcceptedRide
|
||||
- getMapPointsForAllMethods
|
||||
- getPassengerLocationUniversity
|
||||
- getRideStatus
|
||||
- handleActiveRideOnStartup
|
||||
- handleNoDriverFound
|
||||
- isDriversDataValid
|
||||
- lastWhere
|
||||
- onChangedPassengerCount
|
||||
- onChangedPassengersChoose
|
||||
- processRideAcceptance
|
||||
- retrySearchForDrivers
|
||||
- showDrawingBottomSheet
|
||||
- showNoDriversDialog
|
||||
- startSearchingTimer
|
||||
- wait
|
||||
|
||||
--- PRECISE MISSING VARIABLES/FIELDS ---
|
||||
Total original variables: 626
|
||||
Total defined in split controllers: 558
|
||||
Total missing: 97
|
||||
- EdgeInsets
|
||||
- Error
|
||||
- InfoWindow
|
||||
- LatLngBounds
|
||||
- LocationData
|
||||
- R
|
||||
- _buildOsrmWaypointCoords
|
||||
- _calculateDistance
|
||||
- _haversineKm
|
||||
- _isActiveRideState
|
||||
- _isStateProcessing
|
||||
- _isUsingFallback
|
||||
- _kmToLatDelta
|
||||
- _kmToLngDelta
|
||||
- _reconnectTimer
|
||||
- _relevanceScore
|
||||
- a
|
||||
- aerialDistance
|
||||
- apiDistanceMeters
|
||||
- apiKey
|
||||
- attemptCount
|
||||
- c
|
||||
- carInfo
|
||||
- cardNumber
|
||||
- carsOrder
|
||||
- checkPassengerLocation
|
||||
- commissionPct
|
||||
- context
|
||||
- coordDestination
|
||||
- currentAttempt
|
||||
- currentCarType
|
||||
- currentLocationOfDrivers
|
||||
- currentRideId
|
||||
- currentTimeSearchingCaptainWindow
|
||||
- dInfo
|
||||
- dLat
|
||||
- dataCarsLocationByPassenger
|
||||
- datadriverCarsLocationToPassengerAfterApplied
|
||||
- dest
|
||||
- deviation
|
||||
- distanceOfTrip
|
||||
- driverCarPlate
|
||||
- driverLocationToPassenger
|
||||
- driverName
|
||||
- driverOrderStatus
|
||||
- driverPhone
|
||||
- durationByPassenger
|
||||
- dynamicApiUrl
|
||||
- etaText
|
||||
- fName
|
||||
- finalReason
|
||||
- firebaseMessagesController
|
||||
- increaseFeeFormKey
|
||||
- info
|
||||
- isBeginRideFromDriverRunning
|
||||
- isDrawingRoute
|
||||
- isDriversDataValid
|
||||
- isDriversTokensSend
|
||||
- isInUniversity
|
||||
- isRequestValid
|
||||
- kDurationScalar
|
||||
- key
|
||||
- km
|
||||
- kmInDegree
|
||||
- lName
|
||||
- latDest
|
||||
- latestWaypoint
|
||||
- lngDest
|
||||
- lowPerf
|
||||
- mapAPIKEY
|
||||
- messagesFormKey
|
||||
- might
|
||||
- minBillableKm
|
||||
- minFareSYP
|
||||
- newValue
|
||||
- northeast
|
||||
- originCoords
|
||||
- pLower
|
||||
- passengerLocation
|
||||
- passengerLocationStringUnvirsity
|
||||
- placeName
|
||||
- polylineString
|
||||
- previousLocationOfDrivers
|
||||
- progressTimerRideBeginVip
|
||||
- promoFormKey
|
||||
- qLower
|
||||
- query
|
||||
- rLat1
|
||||
- rLat2
|
||||
- ram
|
||||
- rideData
|
||||
- sdk
|
||||
- selectedPassengerCount
|
||||
- southwest
|
||||
- startLng
|
||||
- status
|
||||
- stringElapsedTimeRideBegin
|
||||
214
siro_rider/lib/controller/home/profile/complaint_controller.dart
Normal file
214
siro_rider/lib/controller/home/profile/complaint_controller.dart
Normal file
@@ -0,0 +1,214 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:siro_rider/constant/box_name.dart';
|
||||
import 'package:siro_rider/constant/colors.dart';
|
||||
import 'package:siro_rider/constant/links.dart';
|
||||
import 'package:siro_rider/controller/functions/crud.dart';
|
||||
import 'package:siro_rider/main.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
import '../../../env/env.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../../views/widgets/mydialoug.dart';
|
||||
import '../../functions/encrypt_decrypt.dart';
|
||||
|
||||
class ComplaintController extends GetxController {
|
||||
bool isLoading = false;
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final complaintController = TextEditingController();
|
||||
|
||||
List feedBack = [];
|
||||
Map<String, dynamic>? passengerReport;
|
||||
Map<String, dynamic>? driverReport;
|
||||
|
||||
var isUploading = false.obs;
|
||||
var uploadSuccess = false.obs;
|
||||
String audioLink = ''; // سيتم تخزين رابط الصوت هنا بعد الرفع
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
getLatestRidesForPassengers();
|
||||
}
|
||||
|
||||
// --- دالة مخصصة لعرض إشعارات Snackbar بشكل جميل ---
|
||||
void _showCustomSnackbar(String title, String message,
|
||||
{bool isError = false}) {
|
||||
Get.snackbar(
|
||||
'', // العنوان سيتم التعامل معه عبر titleText
|
||||
'', // الرسالة سيتم التعامل معها عبر messageText
|
||||
titleText: Text(title.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
messageText: Text(message.tr,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14)),
|
||||
backgroundColor: isError
|
||||
? AppColor.redColor.withOpacity(0.95)
|
||||
: const Color.fromARGB(255, 6, 148, 79).withOpacity(0.95),
|
||||
icon: Icon(isError ? Icons.error_outline : Icons.check_circle_outline,
|
||||
color: Colors.white, size: 28),
|
||||
borderRadius: 12,
|
||||
margin: const EdgeInsets.all(15),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
duration: const Duration(seconds: 4),
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
// --- هذه الدالة تبقى كما هي لجلب بيانات الرحلة ---
|
||||
getLatestRidesForPassengers() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
var res = await CRUD().get(link: AppLink.getFeedBack, payload: {
|
||||
'passengerId': box.read(BoxName.passengerID).toString(),
|
||||
});
|
||||
if (res != 'failure') {
|
||||
var d = jsonDecode(res)['message'];
|
||||
feedBack = d;
|
||||
}
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
// --- تم تحديث الهيدر في هذه الدالة ---
|
||||
Future<void> uploadAudioFile(File audioFile) async {
|
||||
try {
|
||||
isUploading.value = true;
|
||||
update();
|
||||
|
||||
var uri = Uri.parse('${AppLink.server}/upload_audio.php');
|
||||
var request = http.MultipartRequest('POST', uri);
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
final String fingerPrint = box.read(BoxName.deviceFpEncrypted)?.toString() ?? '';
|
||||
|
||||
var mimeType = lookupMimeType(audioFile.path);
|
||||
// ** التعديل: تم استخدام نفس هيدر التوثيق **
|
||||
request.headers.addAll({
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-FP': fingerPrint,
|
||||
});
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath(
|
||||
'audio',
|
||||
audioFile.path,
|
||||
contentType: mimeType != null ? MediaType.parse(mimeType) : null,
|
||||
),
|
||||
);
|
||||
|
||||
var response = await request.send();
|
||||
var responseBody = await http.Response.fromStream(response);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var jsonResponse = jsonDecode(responseBody.body);
|
||||
if (jsonResponse['status'] == 'Audio file uploaded successfully.') {
|
||||
uploadSuccess.value = true;
|
||||
audioLink = jsonResponse['link']; // تخزين الرابط في المتغير
|
||||
Get.back();
|
||||
// استخدام الـ Snackbar الجديد
|
||||
_showCustomSnackbar('Success', 'Audio uploaded successfully.');
|
||||
} else {
|
||||
uploadSuccess.value = false;
|
||||
_showCustomSnackbar('Error', 'Failed to upload audio file.',
|
||||
isError: true);
|
||||
}
|
||||
} else {
|
||||
uploadSuccess.value = false;
|
||||
_showCustomSnackbar('Error', 'Server error: ${response.statusCode}',
|
||||
isError: true);
|
||||
}
|
||||
} catch (e) {
|
||||
uploadSuccess.value = false;
|
||||
_showCustomSnackbar(
|
||||
'Error', 'An application error occurred during upload.',
|
||||
isError: true);
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
// --- الدالة الجديدة التي تتصل بالسكريبت الجديد ---
|
||||
Future<void> submitComplaintToServer() async {
|
||||
// 1. التحقق من صحة الفورم
|
||||
if (!formKey.currentState!.validate() || complaintController.text.isEmpty) {
|
||||
// استخدام الـ Snackbar الجديد
|
||||
_showCustomSnackbar(
|
||||
'Error', 'Please describe your issue before submitting.',
|
||||
isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. التحقق من وجود بيانات الرحلة
|
||||
if (feedBack.isEmpty) {
|
||||
// استخدام الـ Snackbar الجديد
|
||||
_showCustomSnackbar(
|
||||
'Error', 'Ride information not found. Please refresh the page.',
|
||||
isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
// 3. استخراج البيانات المطلوبة
|
||||
final rideId = feedBack[0]['id'].toString(); // ! تأكد أن اسم حقل ID صحيح
|
||||
final complaint = complaintController.text;
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
|
||||
// 4. استدعاء سكربت PHP الجديد باستخدام http.post
|
||||
final response = await http.post(
|
||||
Uri.parse(AppLink.add_solve_all),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
body: {
|
||||
'ride_id': rideId,
|
||||
'complaint_text': complaint,
|
||||
'audio_link': audioLink,
|
||||
},
|
||||
);
|
||||
|
||||
Log.print('Server Response: ${response.body}');
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
_showCustomSnackbar(
|
||||
'Error', 'Failed to connect to the server. Please try again.',
|
||||
isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. التعامل مع محتوى الرد من الخادم
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (responseData['status'] == 'success') {
|
||||
passengerReport = responseData['data']['passenger_response'];
|
||||
driverReport = responseData['data']['driver_response'];
|
||||
update();
|
||||
|
||||
MyDialogContent().getDialog(
|
||||
'Success'.tr, Text('Your complaint has been submitted.'.tr), () {
|
||||
Get.back();
|
||||
complaintController.clear();
|
||||
audioLink = '';
|
||||
formKey.currentState?.reset();
|
||||
});
|
||||
} else {
|
||||
String errorMessage =
|
||||
responseData['message'] ?? 'An unknown server error occurred'.tr;
|
||||
_showCustomSnackbar('Submission Failed', errorMessage, isError: true);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Submit Complaint Error: $e");
|
||||
_showCustomSnackbar('Error', 'An application error occurred.'.tr,
|
||||
isError: true);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
351
siro_rider/lib/controller/home/profile/invit_controller.dart
Normal file
351
siro_rider/lib/controller/home/profile/invit_controller.dart
Normal file
@@ -0,0 +1,351 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_rider/constant/box_name.dart';
|
||||
import 'package:siro_rider/constant/colors.dart';
|
||||
import 'package:siro_rider/constant/links.dart';
|
||||
import 'package:siro_rider/controller/functions/crud.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../../views/widgets/error_snakbar.dart';
|
||||
import '../../../views/widgets/mydialoug.dart';
|
||||
import '../../functions/launch.dart';
|
||||
import '../../notification/notification_captain_controller.dart';
|
||||
import '../../payment/payment_controller.dart';
|
||||
|
||||
class InviteController extends GetxController {
|
||||
final TextEditingController invitePhoneController = TextEditingController();
|
||||
List driverInvitationData = [];
|
||||
List driverInvitationDataToPassengers = [];
|
||||
String? couponCode;
|
||||
String? driverCouponCode;
|
||||
|
||||
int selectedTab = 0;
|
||||
PassengerStats passengerStats = PassengerStats();
|
||||
|
||||
List<Contact> contacts = <Contact>[];
|
||||
RxList<Map<String, dynamic>> contactMaps = <Map<String, dynamic>>[].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// It's good practice to fetch initial data in onInit or onReady
|
||||
// fetchDriverStats();
|
||||
// fetchDriverStatsPassengers();
|
||||
}
|
||||
|
||||
void updateSelectedTab(int index) {
|
||||
selectedTab = index;
|
||||
update();
|
||||
}
|
||||
|
||||
// --- Sharing Methods ---
|
||||
|
||||
Future<void> shareDriverCode() async {
|
||||
if (driverCouponCode != null) {
|
||||
final String shareText = '''
|
||||
${'Join Intaleq as a driver using my referral code!'.tr}
|
||||
${'Use code:'.tr} $driverCouponCode
|
||||
${'Download the Intaleq Driver app now and earn rewards!'.tr}
|
||||
''';
|
||||
await Share.share(shareText);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sharePassengerCode() async {
|
||||
if (couponCode != null) {
|
||||
final String shareText = '''
|
||||
${'Get a discount on your first Intaleq ride!'.tr}
|
||||
${'Use my referral code:'.tr} $couponCode
|
||||
${'Download the Intaleq app now and enjoy your ride!'.tr}
|
||||
''';
|
||||
await Share.share(shareText);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Data Fetching ---
|
||||
|
||||
void fetchDriverStats() async {
|
||||
try {
|
||||
var response = await CRUD().get(link: AppLink.getInviteDriver, payload: {
|
||||
"driverId": box.read(BoxName.driverID),
|
||||
});
|
||||
if (response != 'failure') {
|
||||
var data = jsonDecode(response);
|
||||
driverInvitationData = data['message'];
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Error fetching driver stats: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void fetchDriverStatsPassengers() async {
|
||||
try {
|
||||
var response = await CRUD()
|
||||
.get(link: AppLink.getDriverInvitationToPassengers, payload: {
|
||||
"driverId": box.read(BoxName.passengerID),
|
||||
});
|
||||
if (response != 'failure') {
|
||||
var data = jsonDecode(response);
|
||||
driverInvitationDataToPassengers = data['message'];
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Error fetching passenger stats: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Contact Handling ---
|
||||
|
||||
/// **IMPROVEMENT**: This function now filters out contacts without any phone numbers.
|
||||
/// This is the fix for the `RangeError` you were seeing, which happened when the UI
|
||||
/// tried to access the first phone number of a contact that had none.
|
||||
Future<void> pickContacts() async {
|
||||
try {
|
||||
// 1. Check current permission status using permission_handler for better control
|
||||
PermissionStatus status = await Permission.contacts.status;
|
||||
|
||||
// 2. If status is permanently denied, direct user to settings
|
||||
if (status.isPermanentlyDenied) {
|
||||
Get.defaultDialog(
|
||||
title: 'Permission Required'.tr,
|
||||
middleText:
|
||||
'Contact permission is permanently denied. Please enable it in settings to continue.'
|
||||
.tr,
|
||||
textConfirm: 'Settings'.tr,
|
||||
textCancel: 'Cancel'.tr,
|
||||
confirmTextColor: Colors.white,
|
||||
onConfirm: () {
|
||||
openAppSettings();
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Request permission if not already granted
|
||||
if (!status.isGranted) {
|
||||
status = await Permission.contacts.request();
|
||||
}
|
||||
|
||||
// 4. Proceed if granted
|
||||
if (status.isGranted) {
|
||||
// Also call flutter_contacts requestPermission to ensure it's synced (internal state)
|
||||
await FlutterContacts.requestPermission(readonly: true);
|
||||
|
||||
final List<Contact> allContacts =
|
||||
await FlutterContacts.getContacts(withProperties: true);
|
||||
final int totalContactsOnDevice = allContacts.length;
|
||||
|
||||
// **FIX**: Filter contacts to only include those with at least one phone number.
|
||||
contacts = allContacts.where((c) => c.phones.isNotEmpty).toList();
|
||||
final int contactsWithPhones = contacts.length;
|
||||
|
||||
if (contactsWithPhones > 0) {
|
||||
Log.print('Found $contactsWithPhones contacts with phone numbers.');
|
||||
contactMaps.value = contacts.map((contact) {
|
||||
return {
|
||||
'name': contact.displayName,
|
||||
'phones': contact.phones.map((p) => p.number).toList(),
|
||||
'emails': contact.emails.map((e) => e.address).toList(),
|
||||
};
|
||||
}).toList();
|
||||
update();
|
||||
|
||||
// **IMPROVEMENT**: Provide feedback if some contacts were filtered out.
|
||||
if (contactsWithPhones < totalContactsOnDevice) {
|
||||
// Get.snackbar('Contacts Loaded'.tr,
|
||||
// '${'Showing'.tr} $contactsWithPhones ${'of'.tr} $totalContactsOnDevice ${'contacts. Others were hidden because they don\'t have a phone number.'.tr}',
|
||||
// snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
} else {
|
||||
Get.snackbar('No contacts found'.tr,
|
||||
'No contacts with phone numbers were found on your device.'.tr);
|
||||
}
|
||||
} else {
|
||||
Get.snackbar('Permission denied'.tr,
|
||||
'Contact permission is required to pick contacts'.tr);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('Error picking contacts: $e');
|
||||
Get.snackbar(
|
||||
'Error'.tr, 'An error occurred while picking contacts: $e'.tr);
|
||||
}
|
||||
}
|
||||
|
||||
void selectPhone(String phone) {
|
||||
invitePhoneController.text = phone;
|
||||
update();
|
||||
Get.back();
|
||||
}
|
||||
|
||||
/// **IMPROVEMENT**: A new robust function to format phone numbers specifically for Syria (+963).
|
||||
/// It handles various user inputs gracefully to produce a standardized international format.
|
||||
String _formatSyrianPhoneNumber(String phone) {
|
||||
// 1. Remove all non-digit characters to clean the input.
|
||||
String digitsOnly = phone.replaceAll(RegExp(r'\D'), '');
|
||||
|
||||
// 2. If it already starts with the country code, we assume it's correct.
|
||||
if (digitsOnly.startsWith('963')) {
|
||||
return '$digitsOnly';
|
||||
}
|
||||
|
||||
// 3. If it starts with '09' (common local format), remove the leading '0'.
|
||||
if (digitsOnly.startsWith('09')) {
|
||||
digitsOnly = digitsOnly.substring(1);
|
||||
}
|
||||
|
||||
// 4. Prepend the Syrian country code.
|
||||
return '963$digitsOnly';
|
||||
}
|
||||
|
||||
/// **IMPROVEMENT**: This method now uses the new phone formatting logic and
|
||||
/// sends a much-improved, user-friendly WhatsApp message.
|
||||
void sendInviteToPassenger() async {
|
||||
if (invitePhoneController.text.isEmpty ||
|
||||
invitePhoneController.text.length < 9) {
|
||||
mySnackeBarError('Please enter a correct phone'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the new formatting function to ensure the number is correct.
|
||||
String formattedPhoneNumber =
|
||||
_formatSyrianPhoneNumber(invitePhoneController.text);
|
||||
|
||||
var response =
|
||||
await CRUD().post(link: AppLink.addInvitationPassenger, payload: {
|
||||
"driverId": box.read(BoxName.passengerID),
|
||||
"inviterPassengerPhone": formattedPhoneNumber,
|
||||
});
|
||||
|
||||
if (response != 'failure') {
|
||||
var d = response;
|
||||
Get.snackbar('Success'.tr, 'Invite sent successfully'.tr,
|
||||
backgroundColor: Colors.green, colorText: Colors.white);
|
||||
|
||||
// التحقق الديناميكي من مكان البيانات (V1 vs V3)
|
||||
var payload = d['data'] ?? d['message'];
|
||||
|
||||
// إذا كان الـ message نصاً وليس خريطة (Map)، نأخذ البيانات من المستوى الأعلى
|
||||
if (payload is String) {
|
||||
payload = d;
|
||||
}
|
||||
|
||||
String expirationTime = (payload['expirationTime'] ?? '').toString();
|
||||
String inviteCode = (payload['inviteCode'] ?? '').toString();
|
||||
|
||||
// New and improved WhatsApp message for better user engagement.
|
||||
String message =
|
||||
"👋 ${'Hello! I\'m inviting you to try Intaleq.'.tr}\n\n"
|
||||
"🎁 ${'Use my invitation code to get a special gift on your first ride!'.tr}\n\n"
|
||||
"${'Your personal invitation code is:'.tr}\n"
|
||||
"*$inviteCode*\n\n"
|
||||
"⏳ ${'Be sure to use it quickly! This code expires at'.tr} *$expirationTime*.\n\n"
|
||||
"📲 ${'Download the app now:'.tr}\n"
|
||||
"• *Android:* https://play.google.com/store/apps/details?id=com.Intaleq.intaleq\n"
|
||||
"• *iOS:* https://apps.apple.com/st/app/intaleq-rider/id6748075179\n\n"
|
||||
"${'See you on the road!'.tr} 🚗";
|
||||
|
||||
launchCommunication('whatsapp', formattedPhoneNumber, message);
|
||||
invitePhoneController.clear();
|
||||
update();
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Error'.tr, "This phone number has already been invited.".tr,
|
||||
backgroundColor: AppColor.redColor,
|
||||
duration: const Duration(seconds: 4));
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Error sending invite: $e");
|
||||
Get.snackbar(
|
||||
'Error'.tr, 'An unexpected error occurred. Please try again.'.tr,
|
||||
backgroundColor: AppColor.redColor);
|
||||
}
|
||||
}
|
||||
|
||||
// This function is dependent on the `pickContacts` method filtering out contacts without phones.
|
||||
savePhoneToServer() async {
|
||||
for (var contactMap in contactMaps) {
|
||||
// The `pickContacts` function ensures the 'phones' list is not empty here.
|
||||
var phones = contactMap['phones'] as List<String>;
|
||||
var res = await CRUD().post(link: AppLink.savePhones, payload: {
|
||||
"name": contactMap['name'] ?? 'No Name',
|
||||
"phones": phones.first, // Safely access the first phone number
|
||||
"phones2": phones.join(', '),
|
||||
});
|
||||
if (res == 'failure') {
|
||||
Log.print('Failed to save contact: ${contactMap['name']}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onSelectPassengerInvitation(int index) async {
|
||||
try {
|
||||
final invitation = driverInvitationDataToPassengers[index];
|
||||
final tripCount =
|
||||
int.tryParse(invitation['countOfInvitDriver'].toString()) ?? 0;
|
||||
final passengerName = invitation['passengerName'].toString();
|
||||
final isGiftTaken = invitation['isGiftToken'].toString() == '1';
|
||||
|
||||
if (tripCount >= 2) {
|
||||
// Gift can be claimed
|
||||
if (!isGiftTaken) {
|
||||
MyDialog().getDialog(
|
||||
'You deserve the gift'.tr,
|
||||
'${'Claim your 20 LE gift for inviting'.tr} $passengerName!',
|
||||
() async {
|
||||
Get.back(); // Close dialog first
|
||||
await Get.find<PaymentController>().addPassengersWallet('20');
|
||||
await CRUD().post(
|
||||
link: AppLink.updatePassengerGift,
|
||||
payload: {'id': invitation['id']},
|
||||
);
|
||||
NotificationCaptainController().addNotificationCaptain(
|
||||
invitation['passengerInviterId'].toString(),
|
||||
"You have got a gift for invitation".tr,
|
||||
'${"You have earned 20".tr} ${'LE'}',
|
||||
false,
|
||||
);
|
||||
fetchDriverStatsPassengers(); // Refresh list
|
||||
},
|
||||
);
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
"Gift Already Claimed".tr,
|
||||
"You have already received your gift for inviting $passengerName."
|
||||
.tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Gift not yet earned
|
||||
MyDialog().getDialog(
|
||||
'${'Keep it up!'.tr}',
|
||||
'$passengerName ${'has completed'.tr} $tripCount / 2 ${'trips'.tr}. ${"You can claim your gift once they complete 2 trips.".tr}',
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Error in onSelectPassengerInvitation: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PassengerStats {
|
||||
final int totalInvites;
|
||||
final int activeUsers;
|
||||
final double totalEarnings;
|
||||
|
||||
PassengerStats({
|
||||
this.totalInvites = 0,
|
||||
this.activeUsers = 0,
|
||||
this.totalEarnings = 0.0,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_rider/constant/box_name.dart';
|
||||
import 'package:siro_rider/constant/links.dart';
|
||||
import 'package:siro_rider/controller/functions/crud.dart';
|
||||
import 'package:siro_rider/main.dart';
|
||||
|
||||
class OrderHistoryController extends GetxController {
|
||||
List<dynamic> orderHistoryListPassenger = [];
|
||||
bool isloading = true;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
getOrderHistoryByPassenger();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
Future getOrderHistoryByPassenger() async {
|
||||
var res = await CRUD().get(link: AppLink.getRides, payload: {
|
||||
'passenger_id': box.read(BoxName.passengerID).toString(),
|
||||
});
|
||||
if (res.toString() == 'failure') {
|
||||
// Get.snackbar('failure', 'message');
|
||||
isloading = false;
|
||||
update();
|
||||
} else {
|
||||
var jsonDecoded = jsonDecode(res);
|
||||
var rawData = jsonDecoded['data'] ?? jsonDecoded['message'];
|
||||
orderHistoryListPassenger = rawData is List ? rawData : [];
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_rider/constant/box_name.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_rider/constant/links.dart';
|
||||
import 'package:siro_rider/controller/functions/crud.dart';
|
||||
|
||||
import '../../../main.dart';
|
||||
|
||||
class PromosController extends GetxController {
|
||||
List<dynamic> promoList = [];
|
||||
bool isLoading = true;
|
||||
late String promos;
|
||||
@override
|
||||
void onInit() {
|
||||
getPromoByToday();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
Future getPromoByToday() async {
|
||||
var res = await CRUD().get(link: AppLink.getPromoBytody, payload: {
|
||||
'passengerID': box.read(BoxName.passengerID).toString(),
|
||||
});
|
||||
if (res.toString() == 'failure') {
|
||||
// Get.defaultDialog(
|
||||
// title: 'No Promo for today .'.tr,
|
||||
// middleText: '',
|
||||
// titleStyle: AppStyle.title,
|
||||
// confirm: MyElevatedButton(
|
||||
// title: 'Back'.tr,
|
||||
// onPressed: () {
|
||||
// Get.back();
|
||||
// Get.back();
|
||||
// }));
|
||||
isLoading = false;
|
||||
update();
|
||||
} else {
|
||||
var jsonDecoded = jsonDecode(res);
|
||||
|
||||
promoList = jsonDecoded['message'];
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
233
siro_rider/lib/controller/home/splash_screen_controlle.dart
Normal file
233
siro_rider/lib/controller/home/splash_screen_controlle.dart
Normal file
@@ -0,0 +1,233 @@
|
||||
import 'package:siro_rider/print.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:siro_rider/controller/functions/crud.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:siro_rider/constant/box_name.dart';
|
||||
import 'package:siro_rider/onbording_page.dart';
|
||||
import 'package:siro_rider/views/auth/login_page.dart';
|
||||
import 'package:siro_rider/controller/auth/login_controller.dart';
|
||||
import 'package:siro_rider/controller/functions/secure_storage.dart';
|
||||
import 'package:quick_actions/quick_actions.dart';
|
||||
import '../../../constant/notification.dart';
|
||||
import '../../../main.dart';
|
||||
import '../firebase/firbase_messge.dart';
|
||||
import '../firebase/local_notification.dart';
|
||||
import '../functions/encrypt_decrypt.dart';
|
||||
|
||||
class SplashScreenController extends GetxController
|
||||
with GetTickerProviderStateMixin {
|
||||
// ─── انيميشن الـ splash الأصلي ───────────────────────────────────────────
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> titleFadeAnimation,
|
||||
titleScaleAnimation,
|
||||
taglineFadeAnimation,
|
||||
footerFadeAnimation;
|
||||
late Animation<Offset> taglineSlideAnimation;
|
||||
|
||||
// ─── انيميشن الحلقات المدارية ────────────────────────────────────────────
|
||||
late AnimationController _orbitController;
|
||||
late Animation<double> orbitAnimation;
|
||||
|
||||
// ─── انيميشن التوهج المتنفّس ─────────────────────────────────────────────
|
||||
late AnimationController _glowController;
|
||||
late Animation<double> glowAnimation;
|
||||
|
||||
final progress = 0.0.obs;
|
||||
Timer? _progressTimer;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// ── الكنترولر الرئيسي للـ splash ─────────────────────────────────────
|
||||
_animationController = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 2000));
|
||||
|
||||
titleFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut)));
|
||||
|
||||
titleScaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut)));
|
||||
|
||||
taglineFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.3, 0.8, curve: Curves.easeOut)));
|
||||
|
||||
taglineSlideAnimation =
|
||||
Tween<Offset>(begin: const Offset(0, 0.5), end: Offset.zero).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.3, 0.8, curve: Curves.easeOut)));
|
||||
|
||||
footerFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.5, 1.0, curve: Curves.easeOut)));
|
||||
|
||||
// ── كنترولر الدوران المداري — دورة كاملة كل 7 ثوانٍ ─────────────────
|
||||
_orbitController =
|
||||
AnimationController(vsync: this, duration: const Duration(seconds: 7))
|
||||
..repeat();
|
||||
|
||||
orbitAnimation =
|
||||
Tween<double>(begin: 0.0, end: 1.0).animate(_orbitController);
|
||||
|
||||
// ── كنترولر التوهج المتنفّس — نبضة كل ثانيتين ───────────────────────
|
||||
_glowController = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 2000))
|
||||
..repeat(reverse: true);
|
||||
|
||||
glowAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _glowController, curve: Curves.easeInOut));
|
||||
|
||||
_animationController.forward();
|
||||
_initializeApp();
|
||||
}
|
||||
|
||||
Future<void> _initializeApp() async {
|
||||
_startProgressAnimation();
|
||||
|
||||
// Run navigation logic and background services in parallel.
|
||||
final logicFuture = _performNavigationLogic();
|
||||
final backgroundServicesFuture = _initializeBackgroundServices();
|
||||
|
||||
// Ensure the splash screen is visible for a minimum duration.
|
||||
final minTimeFuture = Future.delayed(const Duration(seconds: 3));
|
||||
|
||||
// Wait for all tasks to complete.
|
||||
await Future.wait([logicFuture, backgroundServicesFuture, minTimeFuture]);
|
||||
}
|
||||
|
||||
void _startProgressAnimation() {
|
||||
// Visual timer for the progress bar.
|
||||
const totalTime = 2800; // ms
|
||||
const interval = 50; // ms
|
||||
int elapsed = 0;
|
||||
|
||||
_progressTimer =
|
||||
Timer.periodic(const Duration(milliseconds: interval), (timer) {
|
||||
elapsed += interval;
|
||||
progress.value = (elapsed / totalTime).clamp(0.0, 1.0);
|
||||
if (elapsed >= totalTime) timer.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
/// Initializes all heavy background services while the splash animation is running.
|
||||
Future<void> _initializeBackgroundServices() async {
|
||||
try {
|
||||
await EncryptionHelper.initialize();
|
||||
|
||||
// --- [LAZY LOADING IN ACTION] ---
|
||||
// This `Get.find()` call will create the NotificationController instance
|
||||
// for the first time because it was defined with `lazyPut`.
|
||||
final notificationController = Get.find<NotificationController>();
|
||||
await notificationController.initNotifications();
|
||||
|
||||
// The same happens for FirebaseMessagesController.
|
||||
final fcm = Get.find<FirebaseMessagesController>();
|
||||
await fcm.requestFirebaseMessagingPermission();
|
||||
|
||||
// _scheduleDailyNotifications(notificationController);
|
||||
_initializeQuickActions();
|
||||
} catch (e, st) {
|
||||
CRUD.addError('background_init_error: $e', st.toString(), 'SplashScreen');
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines the next screen based on user's login state.
|
||||
Future<void> _performNavigationLogic() async {
|
||||
await _getPackageInfo();
|
||||
SecureStorage().saveData('iss', 'mobile-app:');
|
||||
|
||||
// ── [OPTIMIZATION] جلب التوكن عند بداية تشغيل التطبيق ────────────────
|
||||
Log.print("SplashScreen: Initializing JWT...");
|
||||
await Get.find<LoginController>().getJWT();
|
||||
|
||||
if (box.read(BoxName.onBoarding) == null) {
|
||||
Get.off(() => OnBoardingPage());
|
||||
} else if (box.read(BoxName.email) != null &&
|
||||
box.read(BoxName.phone) != null &&
|
||||
box.read(BoxName.isVerified) == '1') {
|
||||
// `Get.find()` creates the LoginController instance here.
|
||||
final loginController = Get.find<LoginController>();
|
||||
// The loginController itself will handle navigation via Get.offAll() upon success.
|
||||
await loginController.loginUsingCredentials(
|
||||
box.read(BoxName.passengerID).toString(),
|
||||
box.read(BoxName.email).toString(),
|
||||
);
|
||||
} else {
|
||||
Get.off(() => LoginPage());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _getPackageInfo() async {
|
||||
try {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
box.write(BoxName.packagInfo, info.version);
|
||||
update();
|
||||
} catch (e) {
|
||||
Log.print("Could not get package info: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleDailyNotifications(NotificationController controller) {
|
||||
try {
|
||||
final List<String> msgs = passengerMessages ?? const [];
|
||||
if (msgs.isEmpty) {
|
||||
controller.scheduleNotificationsForSevenDays(
|
||||
'Intaleq', 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.', "tone1");
|
||||
} else {
|
||||
final rnd = Random();
|
||||
final raw = msgs[rnd.nextInt(msgs.length)];
|
||||
final parts = raw.split(':');
|
||||
final title = parts.isNotEmpty ? parts.first.trim() : 'Intaleq';
|
||||
final body = parts.length > 1
|
||||
? parts.sublist(1).join(':').trim()
|
||||
: 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.';
|
||||
controller.scheduleNotificationsForSevenDays(
|
||||
title.isEmpty ? 'Intaleq' : title,
|
||||
body.isEmpty ? 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.' : body,
|
||||
"tone1");
|
||||
}
|
||||
} catch (e, st) {
|
||||
CRUD.addError('notif_init: $e', st.toString(), 'SplashScreen');
|
||||
}
|
||||
}
|
||||
|
||||
void _initializeQuickActions() {
|
||||
final QuickActions quickActions = QuickActions();
|
||||
quickActions.initialize((String shortcutType) {
|
||||
Get.toNamed('/$shortcutType');
|
||||
});
|
||||
|
||||
quickActions.setShortcutItems(<ShortcutItem>[
|
||||
ShortcutItem(
|
||||
type: 'shareApp', localizedTitle: 'Share App'.tr, icon: 'icon_share'),
|
||||
ShortcutItem(
|
||||
type: 'wallet', localizedTitle: 'Wallet'.tr, icon: 'icon_wallet'),
|
||||
ShortcutItem(
|
||||
type: 'profile', localizedTitle: 'Profile'.tr, icon: 'icon_user'),
|
||||
ShortcutItem(
|
||||
type: 'contactSupport',
|
||||
localizedTitle: 'Contact Support'.tr,
|
||||
icon: 'icon_support'),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_progressTimer?.cancel();
|
||||
_animationController.dispose();
|
||||
_orbitController.dispose();
|
||||
_glowController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
107
siro_rider/lib/controller/home/trip_monitor_controller.dart
Normal file
107
siro_rider/lib/controller/home/trip_monitor_controller.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_rider/constant/links.dart';
|
||||
import 'package:siro_rider/controller/functions/crud.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
class TripMonitorController extends GetxController {
|
||||
bool isLoading = false;
|
||||
Map tripData = {};
|
||||
late String rideId;
|
||||
late String driverId;
|
||||
IntaleqMapController? mapController;
|
||||
List myListString = [];
|
||||
late Timer timer;
|
||||
late LatLng parentLocation;
|
||||
String carIcon = 'car';
|
||||
String motoIcon = 'moto';
|
||||
String ladyIcon = 'lady';
|
||||
double rotation = 0;
|
||||
double speed = 0;
|
||||
bool isStyleLoaded = false;
|
||||
|
||||
Set<Marker> markers = {};
|
||||
|
||||
getLocationParent() async {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getLocationParents, payload: {"driver_id": driverId});
|
||||
if (res != 'failure') {
|
||||
tripData = jsonDecode(res);
|
||||
parentLocation = LatLng(
|
||||
double.parse(tripData['message'][0]['latitude'].toString()),
|
||||
double.parse(tripData['message'][0]['longitude'].toString()));
|
||||
rotation = double.parse(tripData['message'][0]['heading'].toString());
|
||||
speed = double.parse(tripData['message'][0]['speed'].toString());
|
||||
|
||||
_updateMarker();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void onMapCreated(IntaleqMapController controller) async {
|
||||
mapController = controller;
|
||||
update();
|
||||
}
|
||||
|
||||
void onStyleLoaded() async {
|
||||
isStyleLoaded = true;
|
||||
mapController?.animateCamera(
|
||||
CameraUpdate.newLatLng(parentLocation),
|
||||
);
|
||||
_updateMarker();
|
||||
|
||||
// Set up a timer or interval to trigger the marker update every 10 seconds.
|
||||
timer = Timer.periodic(const Duration(seconds: 10), (_) async {
|
||||
await getLocationParent();
|
||||
mapController?.animateCamera(CameraUpdate.newLatLng(parentLocation));
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
void _updateMarker() {
|
||||
String iconPath = 'assets/images/car.png';
|
||||
if (tripData['message'] != null && tripData['message'].isNotEmpty) {
|
||||
final model = tripData['message'][0]['model'].toString();
|
||||
final gender = tripData['message'][0]['gender'].toString();
|
||||
if (model.contains('دراجة')) {
|
||||
iconPath = 'assets/images/moto1.png';
|
||||
} else if (gender == 'Female') {
|
||||
iconPath = 'assets/images/lady1.png';
|
||||
}
|
||||
}
|
||||
|
||||
markers = {
|
||||
Marker(
|
||||
markerId: const MarkerId('driver'),
|
||||
position: parentLocation,
|
||||
icon: InlqBitmap.fromAsset(iconPath),
|
||||
rotation: rotation,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
),
|
||||
};
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> init({String? rideId, String? driverId}) async {
|
||||
this.driverId = driverId!;
|
||||
this.rideId = rideId!;
|
||||
await getLocationParent();
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
timer.cancel();
|
||||
mapController = null;
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
324
siro_rider/lib/controller/home/vip_waitting_page.dart
Normal file
324
siro_rider/lib/controller/home/vip_waitting_page.dart
Normal file
@@ -0,0 +1,324 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_rider/constant/box_name.dart';
|
||||
import 'package:siro_rider/constant/colors.dart';
|
||||
import 'package:siro_rider/constant/links.dart';
|
||||
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:siro_rider/main.dart';
|
||||
import 'package:siro_rider/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_rider/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_rider/views/widgets/mycircular.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../constant/style.dart';
|
||||
import '../functions/crud.dart';
|
||||
import '../functions/encrypt_decrypt.dart';
|
||||
|
||||
class VipOrderController extends GetxController {
|
||||
RxBool isLoading = false.obs;
|
||||
final arguments = Get.arguments;
|
||||
RxList<dynamic> tripData = <dynamic>[].obs;
|
||||
RxBool isButtonVisible = false.obs;
|
||||
RxInt countdown = 60.obs;
|
||||
Timer? _countdownTimer;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchOrder();
|
||||
startCountdown();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_countdownTimer?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
Future<void> fetchOrder() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
var mapPassengerController = Get.find<RideLifecycleController>();
|
||||
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getMishwari,
|
||||
payload: {
|
||||
// 'driverId': mapPassengerController.driverIdVip.toString(),
|
||||
'driverId': box.read(BoxName.passengerID).toString(),
|
||||
},
|
||||
);
|
||||
|
||||
if (res != 'failure') {
|
||||
var decodedResponse = jsonDecode(res);
|
||||
if (decodedResponse['message'] is List) {
|
||||
tripData.value = decodedResponse['message'];
|
||||
} else {
|
||||
tripData.clear(); // Ensure empty list if no data
|
||||
// mySnackeBarError('No trip data found');
|
||||
}
|
||||
} else {
|
||||
tripData.clear();
|
||||
// mySnackeBarError('Failed to fetch trip data');
|
||||
}
|
||||
} catch (e) {
|
||||
tripData.clear();
|
||||
// mySnackeBarError('An error occurred: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void startCountdown() {
|
||||
_countdownTimer?.cancel(); // Cancel any existing timer
|
||||
countdown.value = 60;
|
||||
|
||||
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (countdown.value > 0) {
|
||||
countdown.value--;
|
||||
} else {
|
||||
timer.cancel();
|
||||
isButtonVisible.value = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void sendToDriverAgain() {
|
||||
// Reset states
|
||||
isButtonVisible.value = false;
|
||||
fetchOrder(); // Refresh order
|
||||
startCountdown(); // Restart countdown
|
||||
}
|
||||
}
|
||||
|
||||
class VipWaittingPage extends StatelessWidget {
|
||||
VipWaittingPage({super.key});
|
||||
final VipOrderController vipOrderController = Get.put(VipOrderController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: "Waiting VIP".tr,
|
||||
body: [
|
||||
Obx(() {
|
||||
// Loading state
|
||||
if (vipOrderController.isLoading.value) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
// No data state
|
||||
if (vipOrderController.tripData.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'No trip data available'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Data available
|
||||
var data = vipOrderController.tripData[0];
|
||||
|
||||
// Function to get the localized status string
|
||||
String getLocalizedStatus(String status) {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'pending'.tr;
|
||||
case 'accepted':
|
||||
return 'accepted'.tr;
|
||||
case 'begin':
|
||||
return 'begin'.tr;
|
||||
case 'rejected':
|
||||
return 'rejected'.tr;
|
||||
case 'cancelled':
|
||||
return 'cancelled'.tr;
|
||||
default:
|
||||
return 'unknown'.tr;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to get the appropriate status color
|
||||
Color getStatusColor(String status) {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return Colors.yellow;
|
||||
case 'accepted':
|
||||
return Colors.green;
|
||||
case 'begin':
|
||||
return Colors.green;
|
||||
case 'rejected':
|
||||
return Colors.red;
|
||||
case 'cancelled':
|
||||
return Colors.red;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: const EdgeInsets.all(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${'Driver Name:'.tr} ${data['name']}",
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${'Car Plate:'.tr} ${data['car_plate']}",
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
"${'Car Make:'.tr} ${data['make']}",
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
"${'Car Model:'.tr} ${data['model']}",
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
"${"Car Color:".tr} ${data['color']}",
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: Icon(
|
||||
Fontisto.car,
|
||||
size: 80,
|
||||
color: Color(
|
||||
int.parse(
|
||||
data['color_hex'].replaceFirst('#', '0xff'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Divider(),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
color: getStatusColor(data['status']),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
"${'Trip Status:'.tr} ${getLocalizedStatus(data['status'])}",
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${'Scheduled Time:'.tr} ${DateFormat('yyyy-MM-dd hh:mm a').format(DateTime.parse(data['timeSelected']))}",
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
data['status'].toString() != 'begin'
|
||||
? MyElevatedButton(
|
||||
title: "Cancel Trip".tr,
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () {
|
||||
Get.find<RideLifecycleController>().cancelVip(
|
||||
data['token'].toString(),
|
||||
data['id'].toString(),
|
||||
);
|
||||
},
|
||||
)
|
||||
: const SizedBox(),
|
||||
Obx(() {
|
||||
// If countdown is still running, show countdown
|
||||
if (!vipOrderController.isButtonVisible.value) {
|
||||
return Column(
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
value: 1 -
|
||||
(vipOrderController.countdown.value / 60),
|
||||
strokeWidth: 6.0,
|
||||
color: AppColor.greenColor,
|
||||
backgroundColor: AppColor.accentColor,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"${vipOrderController.countdown.value}s ${'remaining'.tr}",
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Once countdown is complete, show "Send to Driver Again" button
|
||||
return MyElevatedButton(
|
||||
title: "Send to Driver Again".tr,
|
||||
kolor: AppColor.greenColor,
|
||||
onPressed: () {
|
||||
Get.find<RideLifecycleController>()
|
||||
.sendToDriverAgain(data['token']);
|
||||
vipOrderController.fetchOrder();
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
data['status'].toString() == 'begin'
|
||||
? MyElevatedButton(
|
||||
title: "Click here to begin your trip\n\nGood luck, "
|
||||
.tr +
|
||||
(box.read(BoxName.name).toString().split(' ')[0])
|
||||
.toString(),
|
||||
kolor: AppColor.greenColor,
|
||||
onPressed: () {
|
||||
final mapPassengerController =
|
||||
Get.find<RideLifecycleController>();
|
||||
mapPassengerController.make = data['make'];
|
||||
mapPassengerController.licensePlate =
|
||||
data['car_plate'];
|
||||
mapPassengerController.model = data['model'];
|
||||
mapPassengerController.driverId = data['driverId'];
|
||||
mapPassengerController.carColor = data['color'];
|
||||
mapPassengerController.driverRate = data['rating'];
|
||||
mapPassengerController.colorHex = data['color_hex'];
|
||||
mapPassengerController.driverPhone = data['phone'];
|
||||
mapPassengerController.driverToken = data['token'];
|
||||
mapPassengerController.driverName =
|
||||
data['name'].toString().split(' ')[0];
|
||||
|
||||
Get.back();
|
||||
|
||||
mapPassengerController.begiVIPTripFromPassenger();
|
||||
},
|
||||
)
|
||||
: const SizedBox()
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
isleading: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user