Files
Siro/siro_rider/lib/views/home/navigation/navigation_view.dart
2026-06-21 02:07:00 +03:00

1238 lines
43 KiB
Dart

import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:siro_rider/env/env.dart';
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 ──────────────────────────────────────────────────────────
Color get _kSurface =>
Get.isDarkMode ? const Color(0xFF0F172A) : const Color(0xFFF8FAFC);
Color get _kCardColor =>
Get.isDarkMode ? const Color(0xFF1E293B) : const Color(0xFFFFFFFF);
Color get _kOnSurface =>
Get.isDarkMode ? const Color(0xFFF1F5F9) : const Color(0xFF0F172A);
Color get _kPrimary => const Color(0xFF2563EB);
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,
onStyleLoaded: c.onStyleLoaded,
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,
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: false,
),
// ── 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 ─────────────────────────────────────
if (!c.isNavigating) _ExploreActionRow(controller: c),
// ── 5. Bottom Panel (Explore Mode) ────────────────────────────
if (!c.isNavigating) _ExploreBottomPanel(controller: c),
// ── 6. Bottom HUD (Active Navigation) ─────────────────────────
if (c.isNavigating) _ActiveBottomHUD(controller: c),
// ── 7. Search Results Dropdown ────────────────────────────────
if (c.placesDestination.isNotEmpty && !c.isNavigating)
_SearchResults(controller: c),
// ── 8. Loading Overlay ────────────────────────────────────────
if (c.isLoading) const _LoadingOverlay(),
// ── 9. Location Picker Overlay (Place Creation) ──────────────
_LocationPickerOverlay(controller: c),
// ── 10. Recenter Button ───────────────────────────────────────
if (!c.isCameraLocked && !c.isNavigating)
Positioned(
right: 16,
bottom: 250,
child: _buildMapFAB(
icon: Icons.my_location_rounded,
onTap: () => c.relockCameraToUser(),
),
),
// ── 11. Navigation Icon (Fixed Center) ────────────────────────
if (c.isNavigating) const _NavigationCenterIcon(),
],
),
),
),
);
}
Widget _buildMapFAB({required IconData icon, required VoidCallback onTap}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: _kCardColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 12,
offset: const Offset(0, 4),
)
],
),
child: Icon(icon, color: _kPrimary, size: 22),
),
);
}
}
// =============================================================================
// NAVIGATION CENTER ICON (FIXED)
// =============================================================================
class _NavigationCenterIcon extends StatelessWidget {
const _NavigationCenterIcon({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: _kPrimary,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
)
],
border: Border.all(color: Colors.white, width: 3),
),
padding: const EdgeInsets.all(12),
child: const Icon(
Icons.navigation_rounded,
color: Colors.white,
size: 32,
),
),
const SizedBox(height: 40), // Adjust to align with map focus
],
),
),
);
}
}
// =============================================================================
// UI 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: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Container(
decoration: BoxDecoration(
color: _kCardColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 24,
offset: const Offset(0, 8),
)
],
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
children: [
IconButton(
icon: Icon(Icons.menu_rounded, color: _kOnSurface, size: 24),
onPressed: () => _showMenuSheet(context),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: controller.placeDestinationController,
onChanged: controller.onSearchChanged,
textInputAction: TextInputAction.search,
style: TextStyle(
fontSize: 16,
color: _kOnSurface,
fontWeight: FontWeight.w600),
decoration: InputDecoration(
hintText: box.read(BoxName.lang) == 'ar'
? 'إلى أين؟'
: 'Where to?',
hintStyle: TextStyle(
color: _kOnSurface.withOpacity(0.4), fontSize: 16),
border: InputBorder.none,
isDense: true,
),
),
),
if (controller.placeDestinationController.text.isNotEmpty ||
controller.routes.isNotEmpty)
IconButton(
icon: Icon(Icons.close_rounded,
color: Colors.red.shade400, size: 22),
onPressed: () => controller.clearEverything(),
)
else
Padding(
padding: const EdgeInsets.only(right: 8),
child: CircleAvatar(
radius: 18,
backgroundColor: _kPrimary,
child: const Icon(Icons.person_rounded,
color: Colors.white, size: 20),
),
),
],
),
),
),
),
);
}
}
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});
@override
Widget build(BuildContext context) {
final bool hasRoutes = controller.routes.isNotEmpty;
final isArabic = box.read(BoxName.lang) == 'ar';
final bottomPad = MediaQuery.of(context).padding.bottom;
if (!hasRoutes) return const SizedBox.shrink();
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: EdgeInsets.only(bottom: bottomPad + 16, top: 16),
decoration: BoxDecoration(
color: _kCardColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 32,
offset: const Offset(0, -4),
)
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Text(
isArabic ? 'المسارات المتاحة' : 'Available Routes',
style: TextStyle(
color: _kOnSurface,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
),
const SizedBox(height: 12),
SizedBox(
height: 90,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12),
scrollDirection: Axis.horizontal,
itemCount: controller.routes.length,
itemBuilder: (context, index) {
final r = controller.routes[index];
final isSelected = controller.selectedRouteIndex == index;
return GestureDetector(
onTap: () => controller.selectRoute(index),
child: Container(
width: 140,
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected ? _kPrimary : _kSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? _kPrimary : Colors.grey.shade300,
width: 1.5,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.route_rounded,
color: isSelected ? Colors.white : _kOnSurface,
),
const SizedBox(height: 6),
Text(
"${(r.distanceM / 1000).toStringAsFixed(1)} km",
style: TextStyle(
color: isSelected ? Colors.white : _kOnSurface,
fontWeight: FontWeight.bold,
),
),
Text(
"${(r.durationS / 60).toInt()} min",
style: TextStyle(
color: isSelected
? Colors.white70
: _kOnSurface.withOpacity(0.6),
fontSize: 12,
),
),
],
),
),
);
},
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: _kPrimary,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
),
onPressed: () {
HapticFeedback.mediumImpact();
controller.startActiveNavigation();
},
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.w700)),
],
),
),
),
),
],
),
),
);
}
}
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: Container(
decoration: BoxDecoration(
color: const Color(0xFF1B5E20),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 16,
offset: const Offset(0, 8))
],
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
shape: BoxShape.circle,
),
child: Icon(controller.currentManeuverIcon,
color: Colors.white, size: 28),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
controller.currentInstruction,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w700),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
controller.distanceToNextStep,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
fontWeight: FontWeight.w600),
),
],
),
),
],
),
),
);
}
}
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: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
width: 70,
height: 70,
decoration: BoxDecoration(
color: _kCardColor,
shape: BoxShape.circle,
border: Border.all(color: Colors.red, width: 3),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10)
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
controller.currentSpeed.toStringAsFixed(0),
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w900,
color: Colors.black,
),
),
const Text(
'km/h',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: Colors.grey,
),
),
],
),
),
const SizedBox(width: 12),
Expanded(
child: Container(
decoration: BoxDecoration(
color: _kCardColor,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 24,
offset: const Offset(0, 8))
],
),
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
controller.arrivalTime,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w900,
color: Color(0xFF2563EB),
),
),
Text(
isArabic
? '${controller.estimatedTimeRemaining} د'
: '${controller.estimatedTimeRemaining} min',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: _kOnSurface,
),
),
Text(
controller.distanceWithUnit,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: _kOnSurface.withOpacity(0.7),
),
),
],
),
const SizedBox(height: 14),
Row(
children: [
_CircleIconBtn(
icon: controller.isMuted
? Icons.volume_off_rounded
: Icons.volume_up_rounded,
color: const Color(0xFFE3F2FD),
iconColor: _kPrimary,
onTap: () => controller.toggleMute()),
const Spacer(),
GestureDetector(
onTap: () => controller.clearRoute(),
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 32),
decoration: BoxDecoration(
color: Colors.red.shade600,
borderRadius: BorderRadius.circular(22),
),
child: Center(
child: Text(
isArabic ? 'إنهاء' : 'End',
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
],
),
),
),
],
),
);
}
}
class _CircleIconBtn extends StatelessWidget {
final IconData icon;
final Color color;
final Color iconColor;
final VoidCallback onTap;
const _CircleIconBtn(
{required this.icon,
required this.color,
required this.iconColor,
required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
onTap();
},
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
child: Icon(icon, color: iconColor, size: 22),
),
);
}
}
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 + 80,
left: 16,
right: 16,
child: Material(
color: _kCardColor,
borderRadius: BorderRadius.circular(16),
elevation: 4,
child: Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.4,
),
child: ListView.separated(
shrinkWrap: true,
itemCount: controller.placesDestination.length,
separatorBuilder: (_, __) =>
Divider(height: 1, color: _kOnSurface.withOpacity(0.1)),
itemBuilder: (context, index) {
final place = controller.placesDestination[index];
return ListTile(
onTap: () => controller.selectDestination(place),
leading: Icon(Icons.location_on_rounded, color: _kPrimary),
title: Text(
place['name'] ?? 'Unknown',
style: TextStyle(
color: _kOnSurface, fontWeight: FontWeight.w600),
),
subtitle: Text(
place['address'] ?? place['formatted_address'] ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: _kOnSurface.withOpacity(0.5), fontSize: 12),
),
);
},
),
),
),
);
}
}
class _LoadingOverlay extends StatelessWidget {
const _LoadingOverlay();
@override
Widget build(BuildContext context) {
return Positioned.fill(
child: Container(
color: Colors.black38,
child: Center(
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: _kCardColor,
borderRadius: BorderRadius.circular(20),
),
child:
const CircularProgressIndicator(color: AppColor.primaryColor),
),
),
),
);
}
}
class _ExploreActionRow extends StatelessWidget {
final NavigationController controller;
const _ExploreActionRow({required this.controller});
@override
Widget build(BuildContext context) {
final bool hasRoutes = controller.routes.isNotEmpty;
final isAr = box.read(BoxName.lang) == 'ar';
final double safeBottom = MediaQuery.of(context).padding.bottom;
if (hasRoutes) return const SizedBox.shrink();
return Positioned(
bottom: safeBottom + 20,
left: 0,
right: 0,
child: 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: () {
HapticFeedback.lightImpact();
mySnackbarInfo(isAr ? 'قريباً...' : 'Coming soon...');
},
),
],
),
),
);
}
}
class _ActionCapsule extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
final bool isPrimary;
const _ActionCapsule({
required this.icon,
required this.label,
required this.onTap,
this.isPrimary = false,
});
@override
Widget build(BuildContext context) {
Color bgColor;
Color textColor;
if (isPrimary) {
bgColor = const Color(0xFF0D47A1).withOpacity(0.9);
textColor = Colors.white;
} else {
bgColor = _kCardColor.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: 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 _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: [
IgnorePointer(
child: Container(color: Colors.black.withOpacity(0.1)),
),
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),
],
),
),
),
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),
),
],
),
),
),
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),
),
),
),
),
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(),
),
),
],
);
}
}
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: _kCardColor,
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: _kOnSurface.withOpacity(0.6), fontSize: 13),
),
const SizedBox(height: 20),
TextField(
controller: nameController,
style: TextStyle(color: _kOnSurface),
decoration: InputDecoration(
labelText: isAr ? 'اسم المكان' : 'Place Name',
labelStyle: TextStyle(color: _kOnSurface.withOpacity(0.6)),
prefixIcon: Icon(Icons.label_rounded,
color: _kOnSurface.withOpacity(0.6)),
filled: true,
fillColor: _kSurface.withOpacity(0.3),
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(height: 16),
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: _kSurface.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.category_rounded,
color: _kOnSurface.withOpacity(0.6)),
const SizedBox(width: 12),
Expanded(
child: Text(
selected != null
? (isAr ? selected['ar']! : selected['en']!)
: (isAr ? 'اختر الفئة' : 'Select Category'),
style: TextStyle(
color: selected != null
? _kOnSurface
: _kOnSurface.withOpacity(0.4),
fontSize: 16,
),
),
),
Icon(Icons.keyboard_arrow_down_rounded,
color: _kOnSurface.withOpacity(0.6)),
],
),
),
);
}),
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: _kOnSurface.withOpacity(0.6))),
),
],
),
),
),
);
}
void _showCategoryPicker(
BuildContext context, Function(Map<String, String>) onSelected) {
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.only(top: 12, bottom: 24),
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: 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;
}
}