fix marker rendering & modernize riding widgets for dark mode - 2026-04-11
This commit is contained in:
@@ -16,8 +16,12 @@ import '../../../controller/functions/tts.dart';
|
||||
import '../../../controller/home/decode_polyline_isolate.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import 'dart:ui';
|
||||
|
||||
class NavigationController extends GetxController {
|
||||
import '../../../services/offline_map_service.dart';
|
||||
|
||||
class NavigationController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
// ==========================================================================
|
||||
// ── Tunables ──────────────────────────────────────────────────────────────
|
||||
// ==========================================================================
|
||||
@@ -57,11 +61,17 @@ class NavigationController extends GetxController {
|
||||
/// Updated every tick via angle-aware lerp to eliminate snap/jitter.
|
||||
double _smoothedHeading = 0.0;
|
||||
|
||||
// Animation for smooth tracking
|
||||
AnimationController? _animController;
|
||||
LatLng? _oldLoc;
|
||||
LatLng? _targetLoc;
|
||||
|
||||
double currentSpeed = 0.0; // km/h
|
||||
double totalDistance = 0.0; // metres accumulated this session
|
||||
|
||||
// MapLibre objects
|
||||
Symbol? carSymbol;
|
||||
Symbol? originSymbol;
|
||||
Symbol? destinationSymbol;
|
||||
Line? remainingRouteLine;
|
||||
Line? traveledRouteLine;
|
||||
@@ -155,14 +165,31 @@ class NavigationController extends GetxController {
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_animController = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 1000));
|
||||
_animController!.addListener(() {
|
||||
if (_oldLoc != null && _targetLoc != null && _mapReady) {
|
||||
final t = _animController!.value;
|
||||
final lat = lerpDouble(_oldLoc!.latitude, _targetLoc!.latitude, t)!;
|
||||
final lng = lerpDouble(_oldLoc!.longitude, _targetLoc!.longitude, t)!;
|
||||
myLocation = LatLng(lat, lng);
|
||||
|
||||
if (isStyleLoaded) {
|
||||
_updateCarMarker();
|
||||
if (_fullRouteCoordinates.isNotEmpty && _cameraLockedToUser) {
|
||||
animateCameraToPosition(myLocation!,
|
||||
bearing: _smoothedHeading,
|
||||
zoom: _targetZoom,
|
||||
tilt: _targetTilt);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
_initialize();
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
await _getCurrentLocationAndStartUpdates();
|
||||
if (!Get.isRegistered<TextToSpeechController>()) {
|
||||
Get.put(TextToSpeechController());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -171,6 +198,7 @@ class NavigationController extends GetxController {
|
||||
_recordTimer?.cancel();
|
||||
_uploadBatchTimer?.cancel();
|
||||
_debounce?.cancel();
|
||||
_animController?.dispose();
|
||||
mapController?.dispose();
|
||||
placeDestinationController.dispose();
|
||||
|
||||
@@ -294,7 +322,10 @@ class NavigationController extends GetxController {
|
||||
}
|
||||
_lastDistanceLocation = newLoc;
|
||||
|
||||
myLocation = newLoc;
|
||||
_oldLoc = myLocation ?? newLoc;
|
||||
_targetLoc = newLoc;
|
||||
_animController?.forward(from: 0.0);
|
||||
|
||||
_lastProcessedLocation = newLoc;
|
||||
heading = position.heading;
|
||||
|
||||
@@ -306,23 +337,22 @@ class NavigationController extends GetxController {
|
||||
|
||||
currentSpeed = position.speed * 3.6;
|
||||
|
||||
if (isStyleLoaded) _updateCarMarker();
|
||||
// Initial visual update if map is fresh
|
||||
if (isStyleLoaded && myLocation == null) _updateCarMarker();
|
||||
|
||||
if (_fullRouteCoordinates.isNotEmpty) {
|
||||
if (_cameraLockedToUser) {
|
||||
animateCameraToPosition(myLocation!,
|
||||
bearing: _smoothedHeading, zoom: _targetZoom, tilt: _targetTilt);
|
||||
}
|
||||
_updateTraveledPolylineSmart(myLocation!);
|
||||
_checkNavigationStep(myLocation!);
|
||||
_updateTraveledPolylineSmart(newLoc);
|
||||
_checkNavigationStep(newLoc);
|
||||
_recomputeETA();
|
||||
|
||||
// ── Off-route auto-recalculate ─────────────────────────────────────
|
||||
_checkOffRoute(myLocation!);
|
||||
_checkOffRoute(newLoc);
|
||||
}
|
||||
|
||||
update();
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
Log.print("Error occurred: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
@@ -498,14 +528,14 @@ class NavigationController extends GetxController {
|
||||
geometry: myLocation,
|
||||
iconImage: 'car_icon',
|
||||
iconSize: 1.0,
|
||||
iconRotate: _smoothedHeading, // ← use smoothed heading
|
||||
iconRotate: _smoothedHeading,
|
||||
));
|
||||
} else {
|
||||
mapController!.updateSymbol(
|
||||
carSymbol!,
|
||||
SymbolOptions(
|
||||
geometry: myLocation,
|
||||
iconRotate: _smoothedHeading, // ← use smoothed heading
|
||||
iconRotate: _smoothedHeading,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -532,6 +562,50 @@ class NavigationController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Safe wrapper for animateCamera Bounds to prevent native std::domain_error crash on iOS.
|
||||
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 {
|
||||
// Ensure the coordinates are valid (at least a small span)
|
||||
final latSpan =
|
||||
(bounds.northeast.latitude - bounds.southwest.latitude).abs();
|
||||
final lngSpan =
|
||||
(bounds.northeast.longitude - bounds.southwest.longitude).abs();
|
||||
|
||||
if (latSpan < 0.0001 && lngSpan < 0.0001) {
|
||||
Log.print(
|
||||
'⚠️ _safeAnimateCameraBounds: Point-sized bounds, zooming to center.');
|
||||
mapController
|
||||
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 16));
|
||||
return;
|
||||
}
|
||||
|
||||
// Small delay for view stabilization
|
||||
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 in Nav: $e');
|
||||
try {
|
||||
await mapController
|
||||
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 14));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
void onUserPanned() {
|
||||
_cameraLockedToUser = false;
|
||||
update();
|
||||
@@ -625,38 +699,90 @@ class NavigationController extends GetxController {
|
||||
// ==========================================================================
|
||||
|
||||
Future<void> getRoute(LatLng origin, LatLng destination) async {
|
||||
// ── Routing Decision: Normal Points -> SaaS, Multi-Stop -> OSRM ──
|
||||
// Note: NavigationController usually handles the active trip (normal points).
|
||||
final Map<String, String> queryParams = {
|
||||
'fromLat': origin.latitude.toString(),
|
||||
'fromLng': origin.longitude.toString(),
|
||||
'toLat': destination.latitude.toString(),
|
||||
'toLng': destination.longitude.toString(),
|
||||
};
|
||||
final saasUri =
|
||||
Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams);
|
||||
|
||||
// Fallback OSRM URL
|
||||
final coords = "${origin.longitude},${origin.latitude};"
|
||||
"${destination.longitude},${destination.latitude}";
|
||||
final url =
|
||||
final osrmUrl =
|
||||
"$_routeApiBaseUrl/$coords?steps=true&overview=full&geometries=polyline";
|
||||
|
||||
try {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
// 1. Try SaaS first
|
||||
http.Response response = await http.get(saasUri, headers: {
|
||||
'x-api-key': 'intaleq_secret_2026',
|
||||
});
|
||||
bool useSaaS = response.statusCode == 200;
|
||||
|
||||
if (!useSaaS) {
|
||||
Log.print("⚠️ SaaS Route failed. Falling back to OSRM...");
|
||||
response = await http.get(Uri.parse(osrmUrl));
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
mySnackbarWarning('تعذر الاتصال بخدمة التوجيه.');
|
||||
return;
|
||||
}
|
||||
|
||||
final data = jsonDecode(response.body);
|
||||
if (data['code'] != 'Ok' || (data['routes'] as List).isEmpty) {
|
||||
final bool isSaaS = useSaaS;
|
||||
|
||||
// ── 2. Data Extraction Logic ──────────────────────────────────
|
||||
String pointsString = "";
|
||||
dynamic mainRoute;
|
||||
|
||||
if (isSaaS) {
|
||||
pointsString = data['points']?.toString() ?? "";
|
||||
mainRoute = data; // SaaS structure is top-level
|
||||
} else {
|
||||
if (data['code'] != 'Ok' || (data['routes'] as List).isEmpty) {
|
||||
mySnackbarWarning('لم يتم العثور على مسار.');
|
||||
return;
|
||||
}
|
||||
mainRoute = data['routes'][0];
|
||||
pointsString = mainRoute['geometry']?.toString() ?? "";
|
||||
}
|
||||
|
||||
if (pointsString.isEmpty) {
|
||||
mySnackbarWarning('لم يتم العثور على مسار.');
|
||||
return;
|
||||
}
|
||||
|
||||
final route = data['routes'][0];
|
||||
_fullRouteCoordinates = await compute<String, List<LatLng>>(
|
||||
decodePolylineIsolate, route['geometry'].toString());
|
||||
decodePolylineIsolate, pointsString);
|
||||
|
||||
_lastTraveledIndexInFullRoute = 0;
|
||||
if (isStyleLoaded) _updatePolylinesSets([], _fullRouteCoordinates);
|
||||
|
||||
final legs = route['legs'] as List;
|
||||
if (legs.isNotEmpty) {
|
||||
routeSteps = List<Map<String, dynamic>>.from(legs[0]['steps'] as List);
|
||||
// ── Offline Cache: Ensure destination area is stored in memory/disk ───
|
||||
if (_fullRouteCoordinates.isNotEmpty) {
|
||||
OfflineMapService.instance
|
||||
.downloadRegion(_fullRouteCoordinates.last, radiusKm: 2.0);
|
||||
}
|
||||
|
||||
// Handle legs/steps & totals
|
||||
final legs = mainRoute['legs'] as List?;
|
||||
if (legs != null && legs.isNotEmpty) {
|
||||
routeSteps = List<Map<String, dynamic>>.from(legs[0]['steps'] as List);
|
||||
_routeTotalDistanceM = (legs[0]['distance'] as num).toDouble();
|
||||
_routeTotalDurationS = (legs[0]['duration'] as num).toDouble();
|
||||
} else {
|
||||
// Fallback for SaaS which might have top-level distance/duration
|
||||
routeSteps = [];
|
||||
_routeTotalDistanceM = (mainRoute['distance'] as num).toDouble();
|
||||
_routeTotalDurationS = (mainRoute['duration'] as num).toDouble();
|
||||
}
|
||||
|
||||
if (_routeTotalDistanceM > 0) {
|
||||
totalDistanceRemaining = _routeTotalDistanceM > 1000
|
||||
? "${(_routeTotalDistanceM / 1000).toStringAsFixed(1)} كم"
|
||||
: "${_routeTotalDistanceM.toStringAsFixed(0)} م";
|
||||
@@ -665,8 +791,6 @@ class NavigationController extends GetxController {
|
||||
estimatedTimeRemaining = minutes > 60
|
||||
? "${(minutes / 60).floor()} س ${minutes % 60} د"
|
||||
: "$minutes د";
|
||||
} else {
|
||||
routeSteps = [];
|
||||
}
|
||||
|
||||
for (final step in routeSteps) {
|
||||
@@ -689,20 +813,18 @@ class NavigationController extends GetxController {
|
||||
Get.find<TextToSpeechController>().speakText(currentInstruction);
|
||||
}
|
||||
|
||||
// ── 5. Camera Update (Safe) ───────────────────────────────────
|
||||
if (_fullRouteCoordinates.length >= 2) {
|
||||
final bounds = _boundsFromLatLngList(_fullRouteCoordinates);
|
||||
final bounds =
|
||||
data['bbox'] != null && (data['bbox'] as List).length == 4
|
||||
? LatLngBounds(
|
||||
southwest: LatLng(data['bbox'][1], data['bbox'][0]),
|
||||
northeast: LatLng(data['bbox'][3], data['bbox'][2]),
|
||||
)
|
||||
: _boundsFromLatLngList(_fullRouteCoordinates);
|
||||
|
||||
final latDiff =
|
||||
(bounds.northeast.latitude - bounds.southwest.latitude).abs();
|
||||
final lngDiff =
|
||||
(bounds.northeast.longitude - bounds.southwest.longitude).abs();
|
||||
|
||||
if (latDiff > 0.0001 || lngDiff > 0.0001) {
|
||||
mapController?.animateCamera(CameraUpdate.newLatLngBounds(bounds,
|
||||
bottom: 220, top: 150, left: 50, right: 50));
|
||||
} else {
|
||||
animateCameraToPosition(_fullRouteCoordinates.first, zoom: 15.0);
|
||||
}
|
||||
await _safeAnimateCameraBounds(bounds,
|
||||
bottom: 220, top: 150, left: 50, right: 50);
|
||||
}
|
||||
|
||||
update();
|
||||
@@ -741,6 +863,7 @@ class NavigationController extends GetxController {
|
||||
await clearRoute(isNewRoute: true);
|
||||
|
||||
if (isStyleLoaded && mapController != null) {
|
||||
// Destination Marker (B)
|
||||
destinationSymbol = await mapController!.addSymbol(SymbolOptions(
|
||||
geometry: destination,
|
||||
iconImage: 'dest_icon',
|
||||
@@ -748,6 +871,15 @@ class NavigationController extends GetxController {
|
||||
textField: infoWindowTitle,
|
||||
textOffset: const Offset(0, 2),
|
||||
));
|
||||
|
||||
// Start Marker (A)
|
||||
if (myLocation != null) {
|
||||
originSymbol = await mapController!.addSymbol(SymbolOptions(
|
||||
geometry: myLocation,
|
||||
iconImage: 'start_icon',
|
||||
iconSize: 1.0,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (myLocation != null) await getRoute(myLocation!, destination);
|
||||
@@ -777,6 +909,10 @@ class NavigationController extends GetxController {
|
||||
await mapController!.removeSymbol(destinationSymbol!);
|
||||
destinationSymbol = null;
|
||||
}
|
||||
if (originSymbol != null && mapController != null) {
|
||||
await mapController!.removeSymbol(originSymbol!);
|
||||
originSymbol = null;
|
||||
}
|
||||
if (remainingRouteLine != null && mapController != null) {
|
||||
await mapController!.removeLine(remainingRouteLine!);
|
||||
remainingRouteLine = null;
|
||||
@@ -807,8 +943,11 @@ class NavigationController extends GetxController {
|
||||
Future<void> _loadCustomIcons() async {
|
||||
if (mapController == null) return;
|
||||
final carBytes = await rootBundle.load('assets/images/car.png');
|
||||
final startBytes = await rootBundle.load('assets/images/A.png');
|
||||
final destBytes = await rootBundle.load('assets/images/b.png');
|
||||
|
||||
await mapController!.addImage('car_icon', carBytes.buffer.asUint8List());
|
||||
await mapController!.addImage('start_icon', startBytes.buffer.asUint8List());
|
||||
await mapController!.addImage('dest_icon', destBytes.buffer.asUint8List());
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,22 @@ import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'navigation_controller.dart';
|
||||
|
||||
// ─── Brand colours ───────────────────────────────────────────────────────────
|
||||
const Color _kBlue = Color(0xFF1A73E8);
|
||||
const Color _kBlueDark = Color(0xFF0D47A1);
|
||||
const Color _kSurface = Color(0xFFFFFFFF);
|
||||
const Color _kText = Color(0xFF1C1C1E);
|
||||
const Color _kSubtext = Color(0xFF6B7280);
|
||||
const Color _kGreen = Color(0xFF34A853);
|
||||
// ─── Theme-aware Brand colours ──────────────────────────────────────────────
|
||||
Color get _kBlue => const Color(0xFF1A73E8);
|
||||
Color get _kBlueDark => const Color(0xFF0D47A1);
|
||||
Color get _kSurface =>
|
||||
Get.isDarkMode ? const Color(0xFF1E1E1E) : const Color(0xFFFFFFFF);
|
||||
Color get _kText =>
|
||||
Get.isDarkMode ? const Color(0xFFF5F5F7) : const Color(0xFF1C1C1E);
|
||||
Color get _kSubtext =>
|
||||
Get.isDarkMode ? Colors.white60 : const Color(0xFF6B7280);
|
||||
Color get _kGreen => const Color(0xFF34A853);
|
||||
Color get _kGlassSurface => Get.isDarkMode
|
||||
? Colors.black.withOpacity(0.7)
|
||||
: Colors.white.withOpacity(0.92);
|
||||
Color get _kGlassBorder => Get.isDarkMode
|
||||
? Colors.white.withOpacity(0.12)
|
||||
: Colors.white.withOpacity(0.5);
|
||||
|
||||
class NavigationView extends StatelessWidget {
|
||||
const NavigationView({super.key});
|
||||
@@ -33,7 +43,9 @@ class NavigationView extends StatelessWidget {
|
||||
onMapCreated: c.onMapCreated,
|
||||
onStyleLoadedCallback: c.onStyleLoaded,
|
||||
onMapLongClick: c.onMapLongPressed,
|
||||
styleString: "assets/style.json",
|
||||
styleString: Get.isDarkMode
|
||||
? "assets/style_dark.json"
|
||||
: "assets/style.json",
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: c.myLocation ?? const LatLng(33.5138, 36.2765),
|
||||
zoom: 16.0,
|
||||
@@ -106,7 +118,7 @@ class _SearchBar extends StatelessWidget {
|
||||
controller: controller.placeDestinationController,
|
||||
onChanged: controller.onSearchChanged,
|
||||
textInputAction: TextInputAction.search,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: _kText,
|
||||
fontWeight: FontWeight.w500),
|
||||
@@ -167,8 +179,10 @@ class _SearchResults extends StatelessWidget {
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: controller.placesDestination.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
Divider(height: 1, color: Colors.grey[100], indent: 56),
|
||||
separatorBuilder: (_, __) => Divider(
|
||||
height: 1,
|
||||
color: Get.isDarkMode ? Colors.white12 : Colors.grey[100],
|
||||
indent: 56),
|
||||
itemBuilder: (_, i) {
|
||||
final place = controller.placesDestination[i];
|
||||
final dist = place['distanceKm'] as double?;
|
||||
@@ -187,8 +201,8 @@ class _SearchResults extends StatelessWidget {
|
||||
color: _kBlue.withOpacity(0.08),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.place_rounded,
|
||||
color: _kBlue, size: 18),
|
||||
child:
|
||||
Icon(Icons.place_rounded, color: _kBlue, size: 18),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -196,7 +210,7 @@ class _SearchResults extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(place['name'] ?? '',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14.5,
|
||||
color: _kText),
|
||||
@@ -222,7 +236,7 @@ class _SearchResults extends StatelessWidget {
|
||||
),
|
||||
child: Text(
|
||||
'${dist.toStringAsFixed(1)} كم',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
color: _kBlue,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600),
|
||||
@@ -259,11 +273,14 @@ class _TurnBanner extends StatelessWidget {
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _kBlueDark,
|
||||
color: Get.isDarkMode
|
||||
? Colors.grey[900]?.withOpacity(0.95)
|
||||
: _kBlueDark,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: _kBlueDark.withOpacity(0.35),
|
||||
color: (Get.isDarkMode ? Colors.black : _kBlueDark)
|
||||
.withOpacity(0.35),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 6)),
|
||||
],
|
||||
@@ -274,14 +291,14 @@ class _TurnBanner extends StatelessWidget {
|
||||
children: [
|
||||
// Turn arrow icon
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Icon(Icons.turn_right_rounded,
|
||||
color: Colors.white, size: 30),
|
||||
color: Colors.white, size: 40),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
|
||||
@@ -293,16 +310,16 @@ class _TurnBanner extends StatelessWidget {
|
||||
Text(
|
||||
controller.distanceToNextStep,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500),
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
controller.currentInstruction,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 19,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.2),
|
||||
maxLines: 2,
|
||||
@@ -429,7 +446,7 @@ class _RouteSummaryCard extends StatelessWidget {
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
decoration: BoxDecoration(
|
||||
color: _kSurface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
boxShadow: [
|
||||
@@ -520,20 +537,20 @@ class _InfoPill extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: color.withOpacity(0.2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 15),
|
||||
const SizedBox(width: 5),
|
||||
Icon(icon, color: color, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
color: color, fontSize: 13.5, fontWeight: FontWeight.w700)),
|
||||
color: color, fontSize: 18, fontWeight: FontWeight.w800)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -577,22 +594,27 @@ class _NavigationHUD extends StatelessWidget {
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F9FA),
|
||||
color: Get.isDarkMode
|
||||
? Colors.white.withOpacity(0.05)
|
||||
: const Color(0xFFF8F9FA),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||||
border: Border.all(
|
||||
color: Get.isDarkMode
|
||||
? Colors.white10
|
||||
: Colors.grey.withOpacity(0.15)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.arrow_forward_rounded,
|
||||
size: 15, color: _kSubtext),
|
||||
const SizedBox(width: 8),
|
||||
size: 20, color: _kSubtext),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
controller.nextInstruction,
|
||||
style: TextStyle(
|
||||
color: _kSubtext,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500),
|
||||
color: _kText,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -636,13 +658,13 @@ class _NavigationHUD extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.stop_rounded,
|
||||
color: Colors.redAccent, size: 16),
|
||||
const SizedBox(width: 5),
|
||||
color: Colors.redAccent, size: 24),
|
||||
const SizedBox(width: 6),
|
||||
const Text('إيقاف',
|
||||
style: TextStyle(
|
||||
color: Colors.redAccent,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700)),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -669,44 +691,61 @@ class _SpeedBadge extends StatelessWidget {
|
||||
final bool fast = kmh > 100;
|
||||
|
||||
return Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 130,
|
||||
left: 14,
|
||||
child: Container(
|
||||
width: 62,
|
||||
height: 62,
|
||||
decoration: BoxDecoration(
|
||||
color: fast ? const Color(0xFFD93025) : _kSurface,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: fast ? Colors.red.withOpacity(0.3) : Colors.grey[200]!,
|
||||
width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.12),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'$kmh',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: fast ? Colors.white : _kText,
|
||||
height: 1),
|
||||
bottom: MediaQuery.of(context).padding.bottom + 150,
|
||||
left: 16,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Circular progress mimicking a speedometer
|
||||
SizedBox(
|
||||
width: 86,
|
||||
height: 86,
|
||||
child: CircularProgressIndicator(
|
||||
value: (kmh / 140.0)
|
||||
.clamp(0.0, 1.0), // Assuming 140 is max speed shown
|
||||
strokeWidth: 6,
|
||||
backgroundColor: Get.isDarkMode
|
||||
? Colors.white10
|
||||
: Colors.grey.withOpacity(0.3),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
fast ? Colors.redAccent : _kBlue),
|
||||
),
|
||||
Text(
|
||||
'كم/س',
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: fast ? Colors.white70 : _kSubtext,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
Container(
|
||||
width: 74,
|
||||
height: 74,
|
||||
decoration: BoxDecoration(
|
||||
color: fast ? const Color(0xFFD93025) : _kSurface,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 6)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'$kmh',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: fast ? Colors.white : _kText,
|
||||
height: 1),
|
||||
),
|
||||
Text(
|
||||
'كم/س',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: fast ? Colors.white70 : _kSubtext,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -735,7 +774,7 @@ class _LoadingOverlay extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation(_kBlue),
|
||||
strokeWidth: 3,
|
||||
),
|
||||
@@ -777,14 +816,14 @@ class _GlassCard extends StatelessWidget {
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.92),
|
||||
color: _kGlassSurface,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.5)),
|
||||
border: Border.all(color: _kGlassBorder),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.07),
|
||||
color: Colors.black.withOpacity(Get.isDarkMode ? 0.4 : 0.07),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4)),
|
||||
offset: const Offset(0, 8)),
|
||||
],
|
||||
),
|
||||
padding: padding,
|
||||
|
||||
Reference in New Issue
Block a user