Update: 2026-06-21 02:07:00

This commit is contained in:
Hamza-Ayed
2026-06-21 02:07:00 +03:00
parent af3dcae5b7
commit b2fae9ec66
23 changed files with 1412 additions and 210 deletions

View File

@@ -1,3 +1,4 @@
import 'dart:io';
import 'package:siro_rider/constant/box_name.dart';
import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart';
@@ -23,29 +24,39 @@ class TextToSpeechController extends GetxController {
// Initialize TTS engine with language check
Future<void> initTts() async {
try {
String langCode = box.read(BoxName.lang) ?? 'en-US';
bool isAvailable = await flutterTts.isLanguageAvailable(langCode);
String langCode = box.read(BoxName.lang) ?? 'ar-SA';
if (langCode == 'ar') langCode = 'ar-SA';
if (langCode == 'en') langCode = 'en-US';
// If language is unavailable, default to 'en-US'
if (!isAvailable) {
langCode = 'en-US';
}
bool isAvailable = await flutterTts.isLanguageAvailable(langCode);
if (!isAvailable) langCode = 'en-US';
await flutterTts.setLanguage(langCode);
await flutterTts.setSpeechRate(0.5); // Adjust speech rate
await flutterTts.setVolume(1.0); // Set volume
await flutterTts.setSpeechRate(0.5);
await flutterTts.setVolume(1.0);
if (Platform.isIOS) {
await flutterTts.setIosAudioCategory(
IosTextToSpeechAudioCategory.playback,
[
IosTextToSpeechAudioCategoryOptions.mixWithOthers,
IosTextToSpeechAudioCategoryOptions.duckOthers,
],
);
}
} catch (error) {
Get.snackbar('Error', 'Failed to initialize TTS: $error');
print('TTS Init Error: $error');
}
}
// Function to speak the given text
// Function to speak the given text (stops current speech first)
Future<void> speakText(String text) async {
if (text.isEmpty) return;
try {
await flutterTts.awaitSpeakCompletion(true);
await flutterTts.stop();
await flutterTts.speak(text);
} catch (error) {
Get.snackbar('Error', 'Failed to speak text: $error');
print('Failed to speak text: $error');
}
}
}

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:math';
import 'package:siro_rider/views/widgets/error_snakbar.dart';
import 'package:flutter/foundation.dart';
@@ -18,7 +17,6 @@ import '../../../controller/home/decode_polyline_isolate.dart';
import '../../../env/env.dart';
import '../../../main.dart';
import '../../../print.dart';
import 'dart:ui';
import '../../../services/offline_map_service.dart';
@@ -38,6 +36,57 @@ class RouteData {
});
}
enum ManeuverSign {
straight(0),
slightRight(3),
right(2),
sharpRight(1),
slightLeft(-3),
left(-2),
sharpLeft(-1),
keepRight(7),
keepLeft(-7),
arrive(4),
roundabout(6),
unknown(0);
final int value;
const ManeuverSign(this.value);
static ManeuverSign fromValue(dynamic v) {
return ManeuverSign.values.firstWhere(
(e) => e.value == v,
orElse: () => ManeuverSign.unknown,
);
}
IconData get icon {
switch (this) {
case ManeuverSign.arrive:
return Icons.place_rounded;
case ManeuverSign.roundabout:
return Icons.roundabout_right_rounded;
case ManeuverSign.right:
case ManeuverSign.keepRight:
return Icons.turn_right_rounded;
case ManeuverSign.slightRight:
return Icons.turn_slight_right_rounded;
case ManeuverSign.left:
case ManeuverSign.keepLeft:
return Icons.turn_left_rounded;
case ManeuverSign.slightLeft:
return Icons.turn_slight_left_rounded;
case ManeuverSign.straight:
case ManeuverSign.unknown:
return Icons.straight_rounded;
case ManeuverSign.sharpRight:
return Icons.turn_sharp_right_rounded;
case ManeuverSign.sharpLeft:
return Icons.turn_sharp_left_rounded;
}
}
}
class NavigationController extends GetxController
with GetSingleTickerProviderStateMixin {
static const Duration _recordInterval = Duration(seconds: 4);
@@ -94,8 +143,8 @@ class NavigationController extends GetxController
String distanceToNextStep = "";
String totalDistanceRemaining = "";
String estimatedTimeRemaining = "";
dynamic currentManeuverModifier = 0;
String arrivalTime = "--:--"; // NEW: For the active navigation HUD
ManeuverSign currentManeuverModifier = ManeuverSign.straight;
String arrivalTime = "--:--";
double _routeTotalDistanceM = 0;
double _routeTotalDurationS = 0;
@@ -269,30 +318,7 @@ class NavigationController extends GetxController
},
];
IconData get currentManeuverIcon {
switch (currentManeuverModifier) {
case 4: // Arrive
return Icons.place_rounded;
case 6: // Roundabout
return Icons.roundabout_right_rounded;
case 2: // Right
return Icons.turn_right_rounded;
case 3: // Slight Right
return Icons.turn_slight_right_rounded;
case -2: // Left
return Icons.turn_left_rounded;
case -1: // Slight Left
return Icons.turn_slight_left_rounded;
case 7: // Keep Right
return Icons.turn_right_rounded;
case -7: // Keep Left
return Icons.turn_left_rounded;
case 0: // Straight
return Icons.straight_rounded;
default:
return Icons.straight_rounded;
}
}
IconData get currentManeuverIcon => currentManeuverModifier.icon;
void toggleMute() {
isMuted = !isMuted;
@@ -430,26 +456,32 @@ class NavigationController extends GetxController
Future<void> onMapLongPressed(Point<double> point, LatLng tappedPoint) async {
HapticFeedback.mediumImpact();
final langCode = box.read(BoxName.lang) ?? 'ar';
Get.dialog(
AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: const Text('بدء الملاحة؟',
style: TextStyle(fontWeight: FontWeight.bold)),
content: const Text('هل تريد الذهاب إلى هذا الموقع؟'),
title: Text(langCode == 'ar' ? 'بدء الملاحة؟' : 'Start Navigation?',
style: const TextStyle(fontWeight: FontWeight.bold)),
content: Text(langCode == 'ar'
? 'هل تريد الذهاب إلى هذا الموقع؟'
: 'Go to this location?'),
actions: [
TextButton(
child: const Text('إلغاء', style: TextStyle(color: Colors.grey)),
child: Text(langCode == 'ar' ? 'إلغاء' : 'Cancel',
style: const TextStyle(color: Colors.grey)),
onPressed: () => Get.back()),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0D47A1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12))),
child:
const Text('اذهب الآن', style: TextStyle(color: Colors.white)),
child: Text(langCode == 'ar' ? 'اذهب الآن' : 'Go now',
style: const TextStyle(color: Colors.white)),
onPressed: () {
Get.back();
startNavigationTo(tappedPoint, infoWindowTitle: 'الموقع المحدد');
startNavigationTo(tappedPoint,
infoWindowTitle:
langCode == 'ar' ? 'الموقع المحدد' : 'Selected location');
},
),
],
@@ -682,7 +714,17 @@ class NavigationController extends GetxController
}
Future<void> _updateCarMarker() async {
// Car marker is now handled natively by myLocationEnabled: true.
if (myLocation == null || !isStyleLoaded) return;
markers.removeWhere((m) => m.markerId.value == 'car');
markers.add(Marker(
markerId: const MarkerId('car'),
position: myLocation!,
icon: InlqBitmap.fromStyleImage('car_icon'),
anchor: const Offset(0.5, 0.5),
flat: true,
rotation: _smoothedHeading,
zIndex: 100,
));
}
void animateCameraToPosition(LatLng position,
@@ -874,7 +916,7 @@ class NavigationController extends GetxController
}
Future<void> getRoute(LatLng origin, LatLng destination,
{bool keepNavigationActive = false}) async {
{bool keepNavigationActive = false, int retryCount = 0}) async {
isLoading = true;
update();
@@ -897,13 +939,22 @@ class NavigationController extends GetxController
Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams);
try {
final response =
await http.get(saasUri, headers: {'x-api-key': Env.mapSaasKey});
final response = await http
.get(saasUri, headers: {'x-api-key': Env.mapSaasKey})
.timeout(const Duration(seconds: 15));
if (response.statusCode != 200) {
if (retryCount < 2) {
await Future.delayed(const Duration(seconds: 2));
return getRoute(origin, destination,
keepNavigationActive: keepNavigationActive,
retryCount: retryCount + 1);
}
isLoading = false;
update();
mySnackbarWarning('تعذر الاتصال بخدمة التوجيه.');
mySnackbarWarning(langCode == 'ar'
? 'تعذر الاتصال بخدمة التوجيه.'
: 'Route service unavailable.');
return;
}
@@ -984,7 +1035,8 @@ class NavigationController extends GetxController
if (routeSteps.isNotEmpty) {
currentInstruction = routeSteps[0]['text'] ?? "";
currentManeuverModifier = routeSteps[0]['sign'] ?? 0;
currentManeuverModifier =
ManeuverSign.fromValue(routeSteps[0]['sign']);
nextInstruction = routeSteps.length > 1
? (langCode == 'ar'
? "ثم ${routeSteps[1]['text']}"
@@ -1026,24 +1078,23 @@ class NavigationController extends GetxController
(_fullRouteCoordinates.length - _lastTraveledIndexInFullRoute) /
_fullRouteCoordinates.length;
final remainingM = _routeTotalDistanceM * fraction;
final remainingS = _routeTotalDurationS * fraction;
// Distance
final String langCode = box.read(BoxName.lang) ?? 'ar';
if (remainingM > 1000) {
totalDistanceRemaining = (remainingM / 1000).toStringAsFixed(1);
// We will handle the unit in the view or provide a unit string here
} else {
totalDistanceRemaining = remainingM.toStringAsFixed(0);
// Time remaining: use current speed if moving, fall back to route estimate
double remainingS = _routeTotalDurationS * fraction;
if (currentSpeed > 5.0) {
final speedEstimate = currentSpeed / 3.6; // km/h → m/s
remainingS = remainingM / speedEstimate;
}
// New variable to hold formatted distance with unit
final String langCode = box.read(BoxName.lang) ?? 'ar';
totalDistanceRemaining = remainingM > 1000
? (remainingM / 1000).toStringAsFixed(1)
: remainingM.toStringAsFixed(0);
distanceWithUnit = _formatDistance(remainingM, langCode);
// Time Remaining
final minutes = (remainingS / 60).round();
estimatedTimeRemaining = minutes.toString();
// Arrival Time Calculation
final arrival = DateTime.now().add(Duration(seconds: remainingS.toInt()));
final h = arrival.hour > 12
? arrival.hour - 12
@@ -1127,7 +1178,8 @@ class NavigationController extends GetxController
// Initialize current instruction if available
if (routeSteps.isNotEmpty && currentStepIndex < routeSteps.length) {
currentInstruction = routeSteps[currentStepIndex]['text'] ?? "";
currentManeuverModifier = routeSteps[currentStepIndex]['sign'] ?? 0;
currentManeuverModifier =
ManeuverSign.fromValue(routeSteps[currentStepIndex]['sign']);
nextInstruction = (currentStepIndex + 1) < routeSteps.length
? (box.read(BoxName.lang) == 'ar'
? "ثم ${routeSteps[currentStepIndex + 1]['text']}"
@@ -1174,7 +1226,7 @@ class NavigationController extends GetxController
_lastTraveledIndexInFullRoute = 0;
currentInstruction = "";
nextInstruction = "";
currentManeuverModifier = "siro";
currentManeuverModifier = ManeuverSign.straight;
distanceToNextStep = "";
totalDistanceRemaining = "";
estimatedTimeRemaining = "";
@@ -1231,7 +1283,8 @@ class NavigationController extends GetxController
final String langCode = box.read(BoxName.lang) ?? 'ar';
if (currentStepIndex < routeSteps.length) {
currentInstruction = routeSteps[currentStepIndex]['text'] ?? "";
currentManeuverModifier = routeSteps[currentStepIndex]['sign'] ?? 0;
currentManeuverModifier =
ManeuverSign.fromValue(routeSteps[currentStepIndex]['sign']);
nextInstruction = (currentStepIndex + 1) < routeSteps.length
? (langCode == 'ar'
? "ثم ${routeSteps[currentStepIndex + 1]['text']}"
@@ -1248,7 +1301,7 @@ class NavigationController extends GetxController
final String langCode = box.read(BoxName.lang) ?? 'ar';
currentInstruction =
langCode == 'ar' ? "لقد وصلت إلى وجهتك" : "You have arrived";
currentManeuverModifier = 4;
currentManeuverModifier = ManeuverSign.arrive;
nextInstruction = "";
distanceToNextStep = "";
isNavigating = false;
@@ -1317,9 +1370,6 @@ class NavigationController extends GetxController
return R * 2 * atan2(sqrt(a), sqrt(1 - a));
}
double _kmToLatDelta(double km) => km / 111.32;
double _kmToLngDelta(double km, double lat) =>
km / (111.32 * cos(lat * pi / 180));
LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
double? x0, x1, y0, y1;
for (final ll in list) {

View File

@@ -10,6 +10,11 @@ import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../main.dart';
import '../../widgets/error_snakbar.dart';
import '../../../views/home/setting_page.dart';
import '../../../views/home/HomePage/about_page.dart';
import '../../../views/home/HomePage/contact_us.dart';
import '../../../views/home/HomePage/share_app_page.dart';
import '../../../views/home/profile/order_history.dart';
import 'navigation_controller.dart';
// ─── Color Palette ──────────────────────────────────────────────────────────
@@ -42,8 +47,8 @@ class NavigationView extends StatelessWidget {
apiKey: Env.mapSaasKey,
onMapCreated: c.onMapCreated,
onStyleLoaded: c.onStyleLoaded,
onLongPress: (pos) => c.onMapLongPressed(Point(0, 0), pos),
onTap: (pos) => c.onMapTapped(Point(0, 0), pos),
onLongPress: (latlng) => c.onMapLongPressed(Point(0, 0), latlng),
onTap: (latlng) => c.onMapTapped(Point(0, 0), latlng),
markers: c.markers,
polylines: c.polylines,
circles: c.circles,
@@ -203,7 +208,7 @@ class _ExploreTopUI extends StatelessWidget {
children: [
IconButton(
icon: Icon(Icons.menu_rounded, color: _kOnSurface, size: 24),
onPressed: () {},
onPressed: () => _showMenuSheet(context),
),
const SizedBox(width: 8),
Expanded(
@@ -252,6 +257,76 @@ class _ExploreTopUI extends StatelessWidget {
}
}
void _showMenuSheet(BuildContext context) {
final isAr = box.read(BoxName.lang) == 'ar';
Get.bottomSheet(
Container(
decoration: BoxDecoration(
color: _kCardColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40, height: 4,
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.3),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
_MenuTile(
icon: Icons.settings_rounded,
label: isAr ? 'الإعدادات' : 'Settings',
onTap: () { Get.back(); Get.to(() => const SettingPage()); },
),
_MenuTile(
icon: Icons.account_balance_wallet_rounded,
label: isAr ? 'المحفظة' : 'Wallet',
onTap: () { Get.back(); Get.toNamed('/wallet'); },
),
_MenuTile(
icon: Icons.history_rounded,
label: isAr ? 'سجل الرحلات' : 'Trip History',
onTap: () { Get.back(); Get.to(() => const OrderHistory()); },
),
_MenuTile(
icon: Icons.headset_mic_rounded,
label: isAr ? 'اتصل بنا' : 'Contact Us',
onTap: () { Get.back(); Get.toNamed('/contactSupport'); },
),
_MenuTile(
icon: Icons.info_outline_rounded,
label: isAr ? 'حول التطبيق' : 'About',
onTap: () { Get.back(); Get.to(() => const AboutPage()); },
),
const SizedBox(height: 12),
],
),
),
);
}
class _MenuTile extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
const _MenuTile({required this.icon, required this.label, required this.onTap});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon, color: _kPrimary, size: 24),
title: Text(label, style: TextStyle(fontWeight: FontWeight.w600, color: _kOnSurface)),
trailing: const Icon(Icons.chevron_right_rounded, color: Colors.grey),
onTap: onTap,
);
}
}
class _ExploreBottomPanel extends StatelessWidget {
final NavigationController controller;
const _ExploreBottomPanel({required this.controller});
@@ -743,7 +818,10 @@ class _ExploreActionRow extends StatelessWidget {
_ActionCapsule(
icon: Icons.bookmark_rounded,
label: isAr ? 'المحفوظات' : 'Saved',
onTap: () {},
onTap: () {
HapticFeedback.lightImpact();
mySnackbarInfo(isAr ? 'قريباً...' : 'Coming soon...');
},
),
],
),