Update: 2026-06-21 02:07:00
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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...');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user