Files
intaleq/lib/views/home/navigation/navigation_view.dart

1612 lines
57 KiB
Dart

import 'dart:math';
import 'dart:ui';
import 'package:Intaleq/constant/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:Intaleq/env/env.dart';
import '../../../constant/box_name.dart';
import '../../../main.dart';
import '../../widgets/error_snakbar.dart';
import 'navigation_controller.dart';
// ─── Color Palette Matching the HTML Specs ──────────────────────────────────
Color get _kSurface =>
Get.isDarkMode ? const Color(0xFF131b2e) : const Color(0xFFfaf8ff);
Color get _kSurfaceContainerLowest =>
Get.isDarkMode ? const Color(0xFF1e293b) : const Color(0xFFffffff);
Color get _kSurfaceContainerHigh =>
Get.isDarkMode ? const Color(0xFF334155) : const Color(0xFFe2e7ff);
Color get _kPrimary => const Color(0xFF000000);
Color get _kPrimaryContainer => const Color(0xFF131b2e);
Color get _kOnPrimaryContainer => const Color(0xFF7c839b);
Color get _kOnSurface =>
Get.isDarkMode ? const Color(0xFFffffff) : const Color(0xFF131b2e);
Color get _kOnSurfaceVariant => const Color(0xFF45464d);
Color get _kErrorContainer => const Color(0xFFffdad6);
Color get _kError => const Color(0xFFba1a1a);
Color get _kOutlineVariant => const Color(0xFFc6c6cd);
class NavigationView extends StatelessWidget {
const NavigationView({super.key});
@override
Widget build(BuildContext context) {
final NavigationController c = Get.put(NavigationController());
return AnnotatedRegion<SystemUiOverlayStyle>(
value: Get.isDarkMode
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
child: Scaffold(
backgroundColor: _kSurface,
body: GetBuilder<NavigationController>(
builder: (_) => Stack(
children: [
// ── 1. Map Layer ──────────────────────────────────────────────
IntaleqMap(
apiKey: Env.mapSaasKey,
onMapCreated: c.onMapCreated,
onLongPress: (pos) => c.onMapLongPressed(
Point(0, 0), pos), // Adapted for IntaleqMap API
onTap: (pos) => c.onMapTapped(Point(0, 0), pos),
markers: c.markers,
polylines: c.polylines,
circles: c.circles,
polygons: c.polygons,
mapType: Get.isDarkMode
? IntaleqMapType.normal
: IntaleqMapType.light,
initialCameraPosition: CameraPosition(
target: c.myLocation ?? const LatLng(33.5138, 36.2765),
zoom: 16.0),
myLocationEnabled: true,
),
// ── 2. Top UI (Explore Mode) ──────────────────────────────────
if (!c.isNavigating) _ExploreTopUI(controller: c),
// ── 3. Top UI (Active Navigation Banner) ──────────────────────
if (c.isNavigating && c.currentInstruction.isNotEmpty)
_ActiveTopInstruction(controller: c),
// ── 4. Explore Action Row (Capsules) ──────────────────────────
if (!c.isNavigating) _ExploreActionRow(controller: c),
// ── 5. Bottom Panel (Explore Mode / Route Setup) ──────────────
if (!c.isNavigating) _ExploreBottomPanel(controller: c),
// ── 6. Bottom HUD (Active Navigation) ─────────────────────────
if (c.isNavigating) _ActiveBottomHUD(controller: c),
// ── 7. Speedometer Badge ──────────────────────────────────────
if (c.isNavigating) _SpeedBadge(speed: c.currentSpeed),
// ── 8. Search Results Dropdown ────────────────────────────────
if (c.placesDestination.isNotEmpty && !c.isNavigating)
_SearchResults(controller: c),
// ── 9. Loading Overlay ────────────────────────────────────────
if (c.isLoading) const _LoadingOverlay(),
// ── 10. Location Picker Overlay (Place Creation) ──────────────
_LocationPickerOverlay(controller: c),
// ── 11. Recenter / Follow Me Button ───────────────────────────
if (!c.isCameraLocked)
Positioned(
right: 16,
bottom: c.isNavigating ? 140 : 250,
child: FloatingActionButton.small(
onPressed: () => c.relockCameraToUser(),
backgroundColor: Colors.white,
child: const Icon(Icons.my_location_rounded,
color: Color(0xFF0D47A1)),
),
),
],
),
),
),
);
}
}
// =============================================================================
// EXPLORE MODE COMPONENTS
// =============================================================================
class _ExploreTopUI extends StatelessWidget {
final NavigationController controller;
const _ExploreTopUI({required this.controller});
@override
Widget build(BuildContext context) {
return Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
bottom: false,
child: Column(
children: [
// Search Pill
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: ClipRRect(
borderRadius: BorderRadius.circular(28),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Container(
decoration: BoxDecoration(
color: _kSurfaceContainerLowest.withOpacity(0.85),
borderRadius: BorderRadius.circular(28),
border: Border.all(
color: Colors.white.withOpacity(0.3), width: 1.5),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.12),
blurRadius: 32,
offset: const Offset(0, 12),
)
],
),
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Row(
children: [
IconButton(
icon: Icon(Icons.menu_rounded,
color: _kOnSurface, size: 26),
onPressed: () {}, // Drawer or Menu logic here
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: controller.placeDestinationController,
onChanged: controller.onSearchChanged,
textInputAction: TextInputAction.search,
style: TextStyle(
fontSize: 18,
color: _kOnSurface,
fontWeight: FontWeight.w700),
decoration: InputDecoration(
hintText: box.read(BoxName.lang) == 'ar'
? 'إلى أين؟'
: 'Where to?',
hintStyle: TextStyle(
color: _kOnSurfaceVariant.withOpacity(0.7),
fontSize: 18,
fontWeight: FontWeight.w600),
border: InputBorder.none,
isDense: true,
contentPadding:
const EdgeInsets.symmetric(vertical: 12),
),
),
),
if (controller
.placeDestinationController.text.isNotEmpty ||
controller.routes.isNotEmpty)
Container(
decoration: const BoxDecoration(
color: Color(0xFFFFEBEE),
shape: BoxShape.circle,
),
child: IconButton(
icon: const Icon(Icons.close_rounded),
color: Colors.red,
onPressed: () => controller.clearEverything(),
),
)
else
// Avatar
const Padding(
padding: EdgeInsets.only(right: 4),
child: CircleAvatar(
radius: 20,
backgroundColor: Color(0xFF0D47A1),
child: Icon(Icons.person_rounded,
color: Colors.white, size: 22),
),
),
],
),
),
),
),
),
],
),
),
);
}
}
class _ExploreBottomPanel extends StatelessWidget {
final NavigationController controller;
const _ExploreBottomPanel({required this.controller});
String _formatDuration(double seconds) {
final mins = (seconds / 60).toInt();
if (mins >= 60) {
final h = mins ~/ 60;
final m = mins % 60;
return box.read(BoxName.lang) == 'ar'
? '$h ساعة ${m > 0 ? '$m د' : ''}'
: '${h}h ${m > 0 ? '${m}m' : ''}';
}
return box.read(BoxName.lang) == 'ar' ? '$mins دقيقة' : '$mins min';
}
String _formatDistance(double meters) {
if (meters >= 1000) {
return box.read(BoxName.lang) == 'ar'
? '${(meters / 1000).toStringAsFixed(1)} كم'
: '${(meters / 1000).toStringAsFixed(1)} km';
}
return box.read(BoxName.lang) == 'ar'
? '${meters.toInt()} م'
: '${meters.toInt()} m';
}
@override
Widget build(BuildContext context) {
final bool hasRoutes = controller.routes.isNotEmpty;
final bool isArabic = box.read(BoxName.lang) == 'ar';
final bottomPad = MediaQuery.of(context).padding.bottom;
if (!hasRoutes && controller.recentLocations.isEmpty) {
return const SizedBox.shrink();
}
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
padding: EdgeInsets.only(bottom: bottomPad + 8),
decoration: BoxDecoration(
color: _kSurfaceContainerLowest.withOpacity(0.92),
boxShadow: const [
BoxShadow(
color: Color(0x14000000),
blurRadius: 48,
offset: Offset(0, -12))
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Minimal Drag Handle
Container(
width: 32,
height: 3,
margin: const EdgeInsets.only(top: 8, bottom: 8),
decoration: BoxDecoration(
color: _kOutlineVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(10))),
// ── Route Selection Cards ──
if (hasRoutes) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children:
List.generate(controller.routes.length, (index) {
final r = controller.routes[index];
final isSelected =
controller.selectedRouteIndex == index;
return _RouteOptionCard(
index: index,
distance: _formatDistance(r.distanceM),
duration: _formatDuration(r.durationS),
isSelected: isSelected,
isArabic: isArabic,
onTap: () => controller.selectRoute(index),
);
}),
),
),
const SizedBox(height: 12),
// Start Button
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
HapticFeedback.mediumImpact();
controller.isNavigating = true;
controller.relockCameraToUser();
controller.update();
},
borderRadius: BorderRadius.circular(16),
child: Ink(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF0D47A1), Color(0xFF1565C0)],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color:
const Color(0xFF0D47A1).withOpacity(0.35),
blurRadius: 16,
offset: const Offset(0, 6))
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.navigation_rounded,
color: Colors.white, size: 20),
const SizedBox(width: 8),
Text(
isArabic
? 'ابدأ الملاحة'
: 'Start Navigation',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w800)),
],
),
),
),
),
),
] else if (!hasRoutes) ...[
const SizedBox.shrink(),
],
],
),
),
),
),
);
}
}
class _RouteOptionCard extends StatelessWidget {
final int index;
final String distance;
final String duration;
final bool isSelected;
final bool isArabic;
final VoidCallback onTap;
const _RouteOptionCard({
required this.index,
required this.distance,
required this.duration,
required this.isSelected,
required this.isArabic,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
HapticFeedback.selectionClick();
onTap();
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF0D47A1)
: _kSurfaceContainerHigh.withOpacity(0.4),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? const Color(0xFF42A5F5).withOpacity(0.4)
: _kOutlineVariant.withOpacity(0.15),
width: isSelected ? 1.5 : 1,
),
),
child: Row(
children: [
// Route icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isSelected
? Colors.white.withOpacity(0.15)
: _kSurfaceContainerHigh.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
index == 0 ? Icons.route_rounded : Icons.alt_route_rounded,
color: isSelected ? Colors.white : _kOnSurfaceVariant,
size: 20,
),
),
const SizedBox(width: 14),
// Route Label
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
index == 0
? (isArabic ? 'أفضل مسار' : 'Best Route')
: (isArabic
? 'مسار بديل $index'
: 'Alternative $index'),
style: TextStyle(
color: isSelected ? Colors.white : _kOnSurface,
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
distance,
style: TextStyle(
color: isSelected
? Colors.white.withOpacity(0.7)
: _kOnSurfaceVariant,
fontSize: 12,
),
),
],
),
),
// Duration (prominent)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withOpacity(0.15)
: const Color(0xFFE3F2FD),
borderRadius: BorderRadius.circular(10),
),
child: Text(
duration,
style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFF0D47A1),
fontSize: 14,
fontWeight: FontWeight.w800,
),
),
),
// Selection indicator
const SizedBox(width: 8),
Icon(
isSelected
? Icons.check_circle_rounded
: Icons.radio_button_unchecked_rounded,
color: isSelected ? Colors.white : _kOutlineVariant,
size: 22,
),
],
),
),
);
}
}
class _CompactRecentPlace extends StatelessWidget {
final String title;
final String subtitle;
final VoidCallback onTap;
const _CompactRecentPlace({
required this.title,
required this.subtitle,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return ListTile(
onTap: onTap,
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 0),
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _kSurfaceContainerHigh.withOpacity(0.5),
shape: BoxShape.circle,
),
child: Icon(Icons.history_rounded, size: 18, color: _kOnSurface),
),
title: Text(title,
style: TextStyle(
fontWeight: FontWeight.w700, color: _kOnSurface, fontSize: 14)),
subtitle: Text(subtitle,
style: TextStyle(color: _kOnSurfaceVariant, fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis),
);
}
}
// =============================================================================
// ACTIVE NAVIGATION MODE COMPONENTS
// =============================================================================
class _ActiveTopInstruction extends StatelessWidget {
final NavigationController controller;
const _ActiveTopInstruction({required this.controller});
@override
Widget build(BuildContext context) {
return Positioned(
top: MediaQuery.of(context).padding.top + 12,
left: 16,
right: 16,
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF1B5E20), Color(0xFF2E7D32)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
border:
Border.all(color: Colors.white.withOpacity(0.2), width: 1),
boxShadow: const [
BoxShadow(
color: Color(0x33000000),
blurRadius: 24,
offset: Offset(0, 12))
],
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row(
children: [
// Direction indicator
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(controller.currentManeuverIcon,
color: Colors.white, size: 36),
),
const SizedBox(width: 16),
// Full instruction text
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
controller.currentInstruction,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w800,
letterSpacing: -0.5),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
controller.distanceToNextStep,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 16,
fontWeight: FontWeight.w700),
),
],
),
),
],
),
),
),
),
);
}
}
class _ActiveBottomHUD extends StatelessWidget {
final NavigationController controller;
const _ActiveBottomHUD({required this.controller});
@override
Widget build(BuildContext context) {
final bottomPad = MediaQuery.of(context).padding.bottom;
final isArabic = box.read(BoxName.lang) == 'ar';
return Positioned(
bottom: bottomPad + 16,
left: 16,
right: 16,
child: ClipRRect(
borderRadius: BorderRadius.circular(32),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Container(
decoration: BoxDecoration(
color: _kSurfaceContainerLowest.withOpacity(0.85),
borderRadius: BorderRadius.circular(32),
border:
Border.all(color: Colors.white.withOpacity(0.4), width: 1.5),
boxShadow: const [
BoxShadow(
color: Color(0x14000000),
blurRadius: 24,
offset: Offset(0, 12))
],
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Stats Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
controller.arrivalTime,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w900,
color: Color(0xFF0D47A1),
),
),
Container(
width: 1.5,
height: 24,
color: _kOutlineVariant.withOpacity(0.3)),
Text(
isArabic
? '${controller.estimatedTimeRemaining} دقيقة'
: '${controller.estimatedTimeRemaining} min',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: _kOnSurface,
),
),
Container(
width: 1.5,
height: 24,
color: _kOutlineVariant.withOpacity(0.3)),
Text(
controller.distanceWithUnit,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: _kOnSurfaceVariant,
),
),
],
),
const SizedBox(height: 16),
// Action Buttons Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_IconBtn(
icon: Icons.report_problem_rounded,
bgColor: const Color(0xFFFFF3E0),
iconColor: Colors.orange.shade800,
onTap: () {},
),
_IconBtn(
icon: controller.isMuted
? Icons.volume_off_rounded
: Icons.volume_up_rounded,
bgColor: const Color(0xFFE3F2FD),
iconColor: const Color(0xFF1976D2),
onTap: () => controller.toggleMute(),
),
_IconBtn(
icon: Icons.add_rounded,
bgColor: const Color(0xFFE8F5E9),
iconColor: const Color(0xFF388E3C),
onTap: () => controller.togglePlaceSelectionMode(),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 16),
child: GestureDetector(
onTap: () => controller.clearRoute(),
child: Container(
height: 48,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFE53935), Color(0xFFC62828)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.red.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 6),
)
],
),
child: Center(
child: Text(
isArabic ? 'إنهاء الملاحة' : 'End Route',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
),
),
),
),
),
],
),
],
),
),
),
),
);
}
}
class _IconBtn extends StatelessWidget {
final IconData icon;
final Color bgColor;
final Color iconColor;
final VoidCallback onTap;
const _IconBtn(
{required this.icon,
required this.bgColor,
required this.iconColor,
required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
onTap();
},
child: Container(
width: 48,
height: 48,
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
color: bgColor,
shape: BoxShape.circle,
),
child: Icon(icon, color: iconColor, size: 24),
),
);
}
}
void _showAddPlaceFormDialog(
BuildContext context, NavigationController controller) {
final nameController = TextEditingController();
final categoryNotifier = ValueNotifier<Map<String, String>?>(null);
final isAr = box.read(BoxName.lang) == 'ar';
Get.dialog(
AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
backgroundColor: _kSurfaceContainerLowest,
title: Row(
children: [
const Icon(Icons.add_business_rounded,
color: Color(0xFF0D47A1), size: 28),
const SizedBox(width: 12),
Text(isAr ? 'إضافة مكان جديد' : 'Add New Place',
style:
TextStyle(color: _kOnSurface, fontWeight: FontWeight.bold)),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
isAr
? "ساهم في تحسين الخريطة بإضافة الأماكن الناقصة."
: "Help improve the map by adding missing places.",
style: TextStyle(color: _kOnSurfaceVariant, fontSize: 13),
),
const SizedBox(height: 20),
TextField(
controller: nameController,
style: TextStyle(color: _kOnSurface),
decoration: InputDecoration(
labelText: isAr ? 'اسم المكان' : 'Place Name',
labelStyle: TextStyle(color: _kOnSurfaceVariant),
prefixIcon:
Icon(Icons.label_rounded, color: _kOnSurfaceVariant),
filled: true,
fillColor: _kSurfaceContainerHigh.withOpacity(0.3),
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(height: 16),
// Category Picker Trigger
ValueListenableBuilder<Map<String, String>?>(
valueListenable: categoryNotifier,
builder: (context, selected, _) {
return InkWell(
onTap: () => _showCategoryPicker(context, (cat) {
categoryNotifier.value = cat;
}),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 16),
decoration: BoxDecoration(
color: _kSurfaceContainerHigh.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _kOutlineVariant.withOpacity(0.5)),
),
child: Row(
children: [
Icon(Icons.category_rounded,
color: _kOnSurfaceVariant),
const SizedBox(width: 12),
Expanded(
child: Text(
selected != null
? (isAr ? selected['ar']! : selected['en']!)
: (isAr ? 'اختر الفئة' : 'Select Category'),
style: TextStyle(
color: selected != null
? _kOnSurface
: _kOnSurfaceVariant,
fontSize: 16,
),
),
),
Icon(Icons.keyboard_arrow_down_rounded,
color: _kOnSurfaceVariant),
],
),
),
);
}),
const SizedBox(height: 24),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0D47A1),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
if (nameController.text.isNotEmpty &&
categoryNotifier.value != null) {
Get.back();
controller.submitNewPlace(
nameController.text, categoryNotifier.value!['id']!);
} else {
mySnackbarWarning(
isAr ? 'يرجى إكمال البيانات' : 'Please fill all fields');
}
},
child: Text(isAr ? 'إرسال' : 'Submit',
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold)),
),
TextButton(
onPressed: () => Get.back(),
child: Text(isAr ? 'إلغاء' : 'Cancel',
style: TextStyle(color: _kOnSurfaceVariant)),
),
],
),
),
),
);
}
void _showCategoryPicker(
BuildContext context, Function(Map<String, String>) onSelected) {
final isAr = box.read(BoxName.lang) == 'ar';
Get.bottomSheet(
Container(
decoration: BoxDecoration(
color: _kSurfaceContainerLowest,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
padding: const EdgeInsets.only(top: 12, bottom: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: _kOutlineVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 16),
Text(isAr ? 'اختر الفئة' : 'Select Category',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: _kOnSurface)),
const SizedBox(height: 16),
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: NavigationController.placeCategories.length,
itemBuilder: (context, index) {
final cat = NavigationController.placeCategories[index];
return ListTile(
leading: Icon(_getIconData(cat['icon']!),
color: const Color(0xFF0D47A1)),
title: Text(isAr ? cat['ar']! : cat['en']!,
style: TextStyle(
color: _kOnSurface, fontWeight: FontWeight.w600)),
onTap: () {
HapticFeedback.lightImpact();
onSelected(cat);
Get.back();
},
);
},
),
),
],
),
),
isScrollControlled: true,
);
}
IconData _getIconData(String name) {
switch (name) {
case 'restaurant':
return Icons.restaurant_rounded;
case 'coffee':
return Icons.coffee_rounded;
case 'shopping_basket':
return Icons.shopping_basket_rounded;
case 'local_pharmacy':
return Icons.local_pharmacy_rounded;
case 'local_gas_station':
return Icons.local_gas_station_rounded;
case 'atm':
return Icons.atm_rounded;
case 'account_balance':
return Icons.account_balance_rounded;
case 'mosque':
return Icons.mosque_rounded;
case 'local_hospital':
return Icons.local_hospital_rounded;
case 'school':
return Icons.school_rounded;
case 'park':
return Icons.park_rounded;
case 'hotel':
return Icons.hotel_rounded;
case 'shopping_mall':
return Icons.store_rounded;
case 'fitness_center':
return Icons.fitness_center_rounded;
case 'content_cut':
return Icons.content_cut_rounded;
case 'bakery_dining':
return Icons.bakery_dining_rounded;
case 'local_laundry_service':
return Icons.local_laundry_service_rounded;
case 'build':
return Icons.build_rounded;
case 'gavel':
return Icons.gavel_rounded;
default:
return Icons.place_rounded;
}
}
class _LocationPickerOverlay extends StatelessWidget {
final NavigationController controller;
const _LocationPickerOverlay({required this.controller});
@override
Widget build(BuildContext context) {
if (!controller.isSelectingPlaceLocation) return const SizedBox.shrink();
final isAr = box.read(BoxName.lang) == 'ar';
return Stack(
children: [
// Dim the background slightly - Non-blocking
IgnorePointer(
child: Container(color: Colors.black.withOpacity(0.1)),
),
// Center Crosshair/Pointer - Non-blocking
IgnorePointer(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4))
],
),
child: const Icon(Icons.add_location_alt_rounded,
color: Color(0xFF0D47A1), size: 40),
),
const SizedBox(height: 40),
],
),
),
),
// Confirm Button
Positioned(
bottom: 110,
left: 32,
right: 32,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1B5E20),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
elevation: 8,
shadowColor: const Color(0xFF1B5E20).withOpacity(0.5),
),
onPressed: () => _showAddPlaceFormDialog(context, controller),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.check_circle_rounded, color: Colors.white),
const SizedBox(width: 12),
Text(
isAr ? 'تأكيد الموقع' : 'Confirm Location',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w800),
),
],
),
),
),
// Help Tooltip
Positioned(
top: 140,
left: 40,
right: 40,
child: IgnorePointer(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(30),
),
child: Text(
isAr
? "حرك الخريطة لتحديد موقع المكان"
: "Move map to pick place location",
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 13),
),
),
),
),
// Cancel Button
Positioned(
top: 60,
right: 20,
child: FloatingActionButton.small(
backgroundColor: Colors.white,
elevation: 4,
child: const Icon(Icons.close_rounded, color: Colors.black87),
onPressed: () => controller.togglePlaceSelectionMode(),
),
),
],
);
}
}
class _StatItem extends StatelessWidget {
final String label;
final String value;
final Color color;
const _StatItem(
{required this.label, required this.value, required this.color});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(label,
style: TextStyle(
fontSize: 10,
color: _kOnSurfaceVariant,
fontWeight: FontWeight.bold)),
Text(value,
style: TextStyle(
fontSize: 16, color: color, fontWeight: FontWeight.w900)),
],
);
}
}
class _ActionButton extends StatelessWidget {
final IconData icon;
final String label;
final Color color;
final Color? iconColor;
final VoidCallback onPressed;
const _ActionButton(
{required this.icon,
required this.label,
required this.color,
this.iconColor,
required this.onPressed});
@override
Widget build(BuildContext context) {
return Column(
children: [
IconButton(
onPressed: onPressed,
icon: Icon(icon, color: iconColor ?? _kOnSurface),
style: IconButton.styleFrom(
backgroundColor: color, fixedSize: const Size(50, 50)),
),
const SizedBox(height: 4),
Text(label, style: const TextStyle(fontSize: 11)),
],
);
}
}
class _SpeedBadge extends StatelessWidget {
final double speed;
const _SpeedBadge({required this.speed});
@override
Widget build(BuildContext context) {
final int kmh = speed.toInt();
// Exact positioning from HTML (bottom-64 left-6, which is approx 256px from bottom)
return Positioned(
bottom: 256,
left: 24,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: _kSurfaceContainerLowest,
shape: BoxShape.circle,
border: Border.all(color: _kPrimary, width: 4),
boxShadow: const [
BoxShadow(
color: Color(0x0F000000), blurRadius: 32, offset: Offset(0, 8))
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$kmh',
style: TextStyle(
color: _kOnSurface,
fontSize: 28,
fontWeight: FontWeight.w900,
height: 1.0)),
Text('km/h',
style: TextStyle(
color: _kOnSurfaceVariant,
fontSize: 10,
fontWeight: FontWeight.w800,
letterSpacing: -0.5,
)),
],
),
),
);
}
}
// =============================================================================
// SHARED UTILITIES
// =============================================================================
class _MapFab extends StatelessWidget {
final IconData icon;
final Color iconColor;
final VoidCallback onTap;
const _MapFab(
{required this.icon, required this.iconColor, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
width: 50, // Slightly smaller
height: 50,
decoration: BoxDecoration(
color: _kSurfaceContainerLowest.withOpacity(0.95),
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Color(0x1A000000),
blurRadius: 16,
offset: Offset(0, 8))
]),
child: Icon(icon, color: iconColor, size: 24),
),
),
),
);
}
}
class _ExploreActionRow extends StatelessWidget {
final NavigationController controller;
const _ExploreActionRow({required this.controller});
@override
Widget build(BuildContext context) {
final bool hasRoutes = controller.routes.isNotEmpty;
final bool hasRecents = controller.recentLocations.isNotEmpty;
final isAr = box.read(BoxName.lang) == 'ar';
final double safeBottom = MediaQuery.of(context).padding.bottom;
final double bottomOffset = safeBottom + 20;
return AnimatedPositioned(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
bottom: bottomOffset,
left: 0,
right: 0,
child: Container(
color: Colors.transparent,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// Row 1: Favorites
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
physics: const BouncingScrollPhysics(),
child: Row(
children: [
_ActionCapsule(
icon: Icons.add_rounded,
label: isAr ? 'إضافة' : 'Add',
onTap: () => controller.togglePlaceSelectionMode(),
isPrimary: true,
),
_ActionCapsule(
icon: Icons.home_rounded,
label: isAr ? 'المنزل' : 'Home',
onTap: () => controller.goToFavorite('home'),
),
_ActionCapsule(
icon: Icons.work_rounded,
label: isAr ? 'العمل' : 'Work',
onTap: () => controller.goToFavorite('work'),
),
_ActionCapsule(
icon: Icons.bookmark_rounded,
label: isAr ? 'المحفوظات' : 'Saved',
onTap: () {},
),
_ActionCapsule(
icon: Icons.flight_rounded,
label: isAr ? 'المطار' : 'Airport',
onTap: () => controller.goToFavorite('airport'),
),
const SizedBox(width: 8),
_MapFab(
icon: Icons.my_location_rounded,
iconColor: controller.isCameraLocked
? const Color(0xFF0D47A1)
: Colors.grey[400]!,
onTap: () {
HapticFeedback.lightImpact();
controller.relockCameraToUser();
},
),
],
),
),
// Row 2: Recent History (if any)
if (hasRecents && !hasRoutes) ...[
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
child: Row(
children: controller.recentLocations.take(5).map((place) {
return _ActionCapsule(
icon: Icons.history_rounded,
label: place['name'] ?? '',
onTap: () => controller.selectDestination(place),
isRecent: true,
);
}).toList(),
),
),
],
const SizedBox(height: 12),
],
),
),
);
}
}
class _ActionCapsule extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
final bool isPrimary;
final bool isRecent;
const _ActionCapsule({
required this.icon,
required this.label,
required this.onTap,
this.isPrimary = false,
this.isRecent = false,
});
@override
Widget build(BuildContext context) {
Color bgColor;
Color textColor;
if (isPrimary) {
bgColor = const Color(0xFF0D47A1).withOpacity(0.9);
textColor = Colors.white;
} else if (isRecent) {
bgColor = _kSurfaceContainerHigh.withOpacity(0.8);
textColor = _kOnSurfaceVariant;
} else {
bgColor = _kSurfaceContainerLowest.withOpacity(0.85);
textColor = _kOnSurface;
}
return Padding(
padding: const EdgeInsets.only(right: 8),
child: GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
onTap();
},
child: ClipRRect(
borderRadius: BorderRadius.circular(50),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(50),
border: Border.all(
color: isRecent
? Colors.transparent
: Colors.white.withOpacity(0.2),
width: 1.2),
boxShadow: const [
BoxShadow(
color: Color(0x14000000),
blurRadius: 8,
offset: Offset(0, 4))
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18, color: textColor),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: textColor,
),
),
],
),
),
),
),
),
);
}
}
class _SearchResults extends StatelessWidget {
final NavigationController controller;
const _SearchResults({required this.controller});
@override
Widget build(BuildContext context) {
return Positioned(
top: MediaQuery.of(context).padding.top + 90,
left: 16,
right: 16,
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
decoration: BoxDecoration(
color: _kSurfaceContainerLowest.withOpacity(0.95),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withOpacity(0.2)),
boxShadow: const [
BoxShadow(
color: Color(0x1A000000),
blurRadius: 32,
offset: Offset(0, 16))
]),
padding: const EdgeInsets.symmetric(vertical: 8),
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: controller.placesDestination.length,
separatorBuilder: (_, __) => Divider(
height: 1,
color: _kOutlineVariant.withOpacity(0.2),
indent: 72),
itemBuilder: (_, i) {
final place = controller.placesDestination[i];
final dist = place['distanceKm'] as double?;
return InkWell(
onTap: () => controller.selectDestination(place),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 16),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: _kSurfaceContainerHigh,
borderRadius: BorderRadius.circular(12)),
child: Icon(Icons.place_rounded,
color: _kOnSurfaceVariant, size: 20)),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(place['name'] ?? '',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: _kOnSurface),
maxLines: 1,
overflow: TextOverflow.ellipsis),
if ((place['address'] ?? '').isNotEmpty)
Text(place['address'],
style: TextStyle(
fontSize: 14,
color: _kOnSurfaceVariant),
maxLines: 1,
overflow: TextOverflow.ellipsis),
],
),
),
if (dist != null) ...[
const SizedBox(width: 12),
Text('${dist.toStringAsFixed(1)} km',
style: TextStyle(
color: _kOnSurfaceVariant,
fontSize: 14,
fontWeight: FontWeight.bold)),
],
],
),
),
);
},
),
),
),
),
),
);
}
}
class _LoadingOverlay extends StatelessWidget {
const _LoadingOverlay();
@override
Widget build(BuildContext context) {
return Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
child: Container(
color: _kPrimaryContainer.withOpacity(0.4),
child: Center(
child: Container(
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: _kSurfaceContainerLowest,
borderRadius: BorderRadius.circular(24),
boxShadow: const [
BoxShadow(
color: Color(0x33000000),
blurRadius: 48,
offset: Offset(0, 16))
]),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Colors.black),
strokeWidth: 4),
const SizedBox(height: 24),
Text('Routing...'.tr,
style: TextStyle(
color: _kOnSurface,
fontSize: 16,
fontWeight: FontWeight.w800)),
],
),
),
),
),
),
);
}
}