MENA region data enrichment scripts` -> Wait, let's keep it concise and clean.

*   *Option 3:* `feat: add tourist landmarks support with UI sheets, data models
This commit is contained in:
Hamza-Ayed
2026-09-21 05:21:44 +03:00
parent cff5a8e1ce
commit dc9a736e26
29 changed files with 13396 additions and 1982 deletions
+157 -58
View File
@@ -22,6 +22,11 @@ import 'widgets/add_place_sheet.dart';
import 'widgets/vehicle_customizer_sheet.dart';
import 'widgets/about_awards_sheet.dart';
import 'widgets/cinematic_3d_studio_sheet.dart';
import 'widgets/siro_drawer_widget.dart';
import 'widgets/tourist_landmarks_sheet.dart';
import 'widgets/settings_sheet.dart';
import 'widgets/system_updates_sheet.dart';
import '../common/siro_toast.dart';
class MapView extends StatefulWidget {
const MapView({super.key});
@@ -31,6 +36,7 @@ class MapView extends StatefulWidget {
}
class _MapViewState extends State<MapView> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode();
bool _isSearchFocused = false;
@@ -117,6 +123,36 @@ class _MapViewState extends State<MapView> {
);
}
void _showTouristLandmarks(BuildContext context, NavigationCubit cubit, NavigationState state) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => TouristLandmarksSheet(
cubit: cubit,
state: state,
),
);
}
void _showSettingsSheet(BuildContext context, NavigationCubit cubit) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => SettingsSheet(cubit: cubit),
);
}
void _showSystemUpdatesSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const SystemUpdatesSheet(),
);
}
void _showCinematic3DStudio(BuildContext context, NavigationCubit cubit, NavigationState state) {
showModalBottomSheet(
context: context,
@@ -170,14 +206,10 @@ class _MapViewState extends State<MapView> {
position: initialLocation,
);
if (mounted && ok) {
messenger.showSnackBar(
const SnackBar(
content: Text(
'تم إرسال البلاغ بنجاح وتحديث الشبكة التشاركية.',
style: TextStyle(fontSize: 12),
),
backgroundColor: AppColors.tacticalEmerald,
),
_showLuxuryNotification(
context,
message: 'تم إرسال البلاغ بنجاح وتحديث الشبكة التشاركية.',
isError: false,
);
}
},
@@ -196,21 +228,16 @@ class _MapViewState extends State<MapView> {
backgroundColor: Colors.transparent,
builder: (_) => AddPlaceSheet(
onSubmit: (name, cat) async {
final messenger = ScaffoldMessenger.of(context);
final ok = await cubit.submitPlace(
name,
cat,
position: initialLocation,
);
if (mounted) {
messenger.showSnackBar(
SnackBar(
content: Text(
ok ? 'تمت إضافة المكان بنجاح! شكراً لمساهمتك.' : 'تعذر حفظ المكان، يرجى المحاولة لاحقاً.',
style: const TextStyle(fontSize: 12),
),
backgroundColor: ok ? AppColors.tacticalEmerald : AppColors.coralDanger,
),
_showLuxuryNotification(
context,
message: ok ? 'تمت إضافة المكان بنجاح! شكراً لمساهمتك.' : 'تعذر حفظ المكان، يرجى المحاولة لاحقاً.',
isError: !ok,
);
}
},
@@ -222,6 +249,18 @@ class _MapViewState extends State<MapView> {
});
}
void _showLuxuryNotification(
BuildContext context, {
required String message,
bool isError = false,
}) {
if (isError) {
SiroToast.showError(context, message);
} else {
SiroToast.showSuccess(context, message);
}
}
void _showPlaceGatesSheet(BuildContext context, NavigationCubit cubit, PlaceModel place) {
showModalBottomSheet(
context: context,
@@ -255,14 +294,10 @@ class _MapViewState extends State<MapView> {
return BlocConsumer<NavigationCubit, NavigationState>(
listener: (context, state) {
if (state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
state.errorMessage!,
style: const TextStyle(fontSize: 12),
),
backgroundColor: AppColors.coralDanger,
),
_showLuxuryNotification(
context,
message: state.errorMessage!,
isError: true,
);
}
@@ -313,8 +348,21 @@ class _MapViewState extends State<MapView> {
valueListenable: PipService.instance.isInPipMode,
builder: (context, isInPip, _) {
return Scaffold(
key: _scaffoldKey,
resizeToAvoidBottomInset: false,
backgroundColor: AppColors.canvasLight,
drawer: SiroDrawerWidget(
cubit: cubit,
state: state,
onOpenTouristLandmarks: () => _showTouristLandmarks(context, cubit, state),
onOpen3DStudio: () => _showCinematic3DStudio(context, cubit, state),
onOpenLayers: () => _showLayerSelector(context, cubit, state.mapTheme),
onOpenSettings: () => _showSettingsSheet(context, cubit),
onOpenUpdates: () => _showSystemUpdatesSheet(context),
onOpenAbout: () => _showAboutAwardsSheet(context),
onStartReportHazard: () => cubit.startLocationPicking('hazard'),
onStartAddPlace: () => cubit.startLocationPicking('place'),
),
body: SizedBox.expand(
child: Stack(
fit: StackFit.expand,
@@ -375,7 +423,7 @@ class _MapViewState extends State<MapView> {
_searchController.clear();
cubit.clearSearch();
},
onMenuTap: () => _showLayerSelector(context, cubit, state.mapTheme),
onMenuTap: () => _scaffoldKey.currentState?.openDrawer(),
),
const SizedBox(height: 10),
ExplorePanelWidget(
@@ -383,6 +431,7 @@ class _MapViewState extends State<MapView> {
_searchController.text = q;
cubit.searchImmediately(q);
},
onTouristSelected: () => _showTouristLandmarks(context, cubit, state),
),
],
),
@@ -1276,38 +1325,6 @@ class _MapViewState extends State<MapView> {
onTap: () => _showCinematic3DStudio(context, cubit, state),
),
const SizedBox(height: 12),
// Layer Selector Button
_buildFloatingCircle(
icon: Icons.layers_rounded,
color: AppColors.textPrimary,
tooltip: 'طبقات الخريطة',
onTap: () => _showLayerSelector(context, cubit, state.mapTheme),
),
const SizedBox(height: 12),
// Siro Map Platform Architecture & Info Button
_buildFloatingCircle(
icon: Icons.info_outline_rounded,
color: AppColors.appleBlue,
tooltip: 'عن سيرو ماب والمنظومة التقنية',
onTap: () => _showAboutAwardsSheet(context),
),
const SizedBox(height: 12),
// Add Place Button
_buildFloatingCircle(
icon: Icons.add_location_alt_rounded,
color: AppColors.appleBlue,
tooltip: 'إضافة مكان',
onTap: () => cubit.startLocationPicking('place'),
),
const SizedBox(height: 12),
// Report Hazard Button
_buildFloatingCircle(
icon: Icons.warning_amber_rounded,
color: AppColors.sovereignGold,
tooltip: 'إبلاغ عن حالة طريق',
onTap: () => cubit.startLocationPicking('hazard'),
),
const SizedBox(height: 12),
// Recenter GPS Button
_buildFloatingCircle(
icon: Icons.my_location_rounded,
@@ -1374,6 +1391,88 @@ class _MapViewState extends State<MapView> {
),
),
// ── 6c. FLOATING 3D ORBIT STATUS & STOP BUTTON ──
if (!isInPip && state.isOrbiting)
Positioned(
left: 16,
bottom: (state.speed > 3.0) ? 100 : 32,
child: SafeArea(
top: false,
child: Material(
color: Colors.transparent,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF1D1D1F).withValues(alpha: 0.94),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: const Color(0xFFEA580C).withValues(alpha: 0.8),
width: 1.5,
),
boxShadow: const [
BoxShadow(
color: Color(0x3D000000),
blurRadius: 16,
offset: Offset(0, 4),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () => cubit.stopOrbit(),
borderRadius: BorderRadius.circular(12),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.stop_circle_rounded,
color: AppColors.coralDanger,
size: 18,
),
SizedBox(width: 6),
Text(
'إيقاف الدوران 360°',
style: TextStyle(
fontFamily: '.SF Pro Text',
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
],
),
),
const SizedBox(width: 10),
Container(
width: 1,
height: 16,
color: Colors.white.withValues(alpha: 0.2),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () => _showCinematic3DStudio(context, cubit, state),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
shape: BoxShape.circle,
),
child: const Icon(
Icons.tune_rounded,
color: Colors.white,
size: 14,
),
),
),
],
),
),
),
),
),
// ── 7. LOADING OVERLAY ──
if (state.status == NavigationStatus.loading)
Container(
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,16 @@ import '../../../../core/constants/app_colors.dart';
class ExplorePanelWidget extends StatelessWidget {
final Function(String query) onCategorySelected;
final VoidCallback? onTouristSelected;
const ExplorePanelWidget({super.key, required this.onCategorySelected});
const ExplorePanelWidget({
super.key,
required this.onCategorySelected,
this.onTouristSelected,
});
static const List<Map<String, dynamic>> _categories = [
{'title': 'معالم سياحية 🏛️', 'query': '__tourist__', 'icon': Icons.travel_explore_rounded, 'isSpecial': true},
{'title': 'مولات', 'query': 'مول', 'icon': Icons.local_mall_rounded},
{'title': 'مستشفيات', 'query': 'مستشفى', 'icon': Icons.local_hospital_rounded},
{'title': 'مطاعم', 'query': 'مطعم', 'icon': Icons.restaurant_rounded},
@@ -28,28 +34,38 @@ class ExplorePanelWidget extends StatelessWidget {
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final cat = _categories[index];
final isSpecial = cat['isSpecial'] == true;
return ActionChip(
avatar: Icon(
cat['icon'] as IconData,
size: 16,
color: AppColors.appleBlue,
color: isSpecial ? const Color(0xFFD97706) : AppColors.appleBlue,
),
label: Text(
cat['title'] as String,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
fontWeight: isSpecial ? FontWeight.w700 : FontWeight.w600,
color: isSpecial ? const Color(0xFF92400E) : AppColors.textPrimary,
),
),
backgroundColor: AppColors.pureWhite,
side: const BorderSide(color: AppColors.borderSubtle),
backgroundColor: isSpecial ? const Color(0xFFFEF3C7) : AppColors.pureWhite,
side: BorderSide(
color: isSpecial ? const Color(0xFFFDE68A) : AppColors.borderSubtle,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
elevation: 1,
shadowColor: Colors.black.withValues(alpha: 0.08),
onPressed: () => onCategorySelected(cat['query'] as String),
onPressed: () {
if (cat['query'] == '__tourist__' && onTouristSelected != null) {
onTouristSelected!();
} else {
onCategorySelected(cat['query'] as String);
}
},
);
},
),
@@ -0,0 +1,345 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../core/constants/app_colors.dart';
import '../../../../logic/cubits/navigation/navigation_cubit.dart';
import '../../common/siro_toast.dart';
class SettingsSheet extends StatefulWidget {
final NavigationCubit cubit;
const SettingsSheet({super.key, required this.cubit});
@override
State<SettingsSheet> createState() => _SettingsSheetState();
}
class _SettingsSheetState extends State<SettingsSheet> {
bool _avoidTolls = false;
bool _avoidHighways = false;
bool _ecoRouting = true;
bool _autoNightMode = true;
String _unit = 'km';
String _cacheSize = '42.8 ميغابايت';
@override
void initState() {
super.initState();
_loadPreferences();
}
Future<void> _loadPreferences() async {
try {
final prefs = await SharedPreferences.getInstance();
setState(() {
_avoidTolls = prefs.getBool('siro_pref_avoid_tolls') ?? false;
_avoidHighways = prefs.getBool('siro_pref_avoid_highways') ?? false;
_ecoRouting = prefs.getBool('siro_pref_eco_routing') ?? true;
_autoNightMode = prefs.getBool('siro_pref_auto_night') ?? true;
_unit = prefs.getString('siro_pref_unit') ?? 'km';
});
} catch (_) {}
}
Future<void> _saveBool(String key, bool val) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(key, val);
} catch (_) {}
}
Future<void> _clearCache() async {
HapticFeedback.mediumImpact();
setState(() {
_cacheSize = '0.0 ميغابايت';
});
if (mounted) {
SiroToast.showSuccess(
context,
'تم مسح الذاكرة المؤقتة للخرائط بنجاح.',
title: 'تنظيف الذاكرة',
);
}
}
@override
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.85,
),
decoration: const BoxDecoration(
color: AppColors.pureWhite,
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
boxShadow: [
BoxShadow(
color: Color(0x33000000),
blurRadius: 30,
offset: Offset(0, -6),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Drag handle
Center(
child: Container(
margin: const EdgeInsets.only(top: 12, bottom: 8),
width: 44,
height: 5,
decoration: BoxDecoration(
color: AppColors.borderSubtle,
borderRadius: BorderRadius.circular(3),
),
),
),
// Header
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.surfaceMuted,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.borderSubtle),
),
child: const Icon(Icons.settings_suggest_rounded,
color: AppColors.textPrimary, size: 22),
),
const SizedBox(width: 14),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'إعدادات المنظومة وتفضيلات الملاحة',
style: TextStyle(
fontSize: 16.5,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
fontFamily: '.SF Pro Text',
),
),
SizedBox(height: 2),
Text(
'تخصيص قواعد التوجيه، وحدات القياس، والذاكرة المؤقتة',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
fontFamily: '.SF Pro Text',
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close_rounded,
color: AppColors.textMuted),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
const Divider(height: 12, color: AppColors.borderSubtle),
// Settings Body
Expanded(
child: ListView(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
children: [
// ── Section 1: تفضيلات التوجيه الذكي ──
_buildSectionHeader(
'تفضيلات التوجيه والمسارات', Icons.alt_route_rounded),
const SizedBox(height: 8),
_buildSwitchTile(
title: 'المسار الاقتصادي الموفر للوقود',
subtitle:
'حساب المسارات بناءً على استواء التضاريس لتقليل استهلاك الوقود والانبعاثات',
value: _ecoRouting,
onChanged: (val) {
setState(() => _ecoRouting = val);
_saveBool('siro_pref_eco_routing', val);
},
),
_buildSwitchTile(
title: 'تجنب الطرق ذات الرسوم (Tolls)',
subtitle: 'تفضيل الطرق المجانية عند اقتراح المسارات البديلة',
value: _avoidTolls,
onChanged: (val) {
setState(() => _avoidTolls = val);
_saveBool('siro_pref_avoid_tolls', val);
},
),
_buildSwitchTile(
title: 'تجنب الطرق السريعة (Highways)',
subtitle: 'اختيار الطرق المحلية والشوارع الحضرية الهادئة',
value: _avoidHighways,
onChanged: (val) {
setState(() => _avoidHighways = val);
_saveBool('siro_pref_avoid_highways', val);
},
),
const SizedBox(height: 20),
// ── Section 2: العرض والنمط الليلي ──
_buildSectionHeader(
'العرض والنمط البصري', Icons.palette_rounded),
const SizedBox(height: 8),
_buildSwitchTile(
title: 'التبديل التلقائي للنمط الليلي',
subtitle:
'تحويل الخريطة لنمط Night / Tactical تلقائياً بعد الغروب',
value: _autoNightMode,
onChanged: (val) {
setState(() => _autoNightMode = val);
_saveBool('siro_pref_auto_night', val);
},
),
const SizedBox(height: 20),
// ── Section 3: الذاكرة والتخزين المؤقت ──
_buildSectionHeader(
'الذاكرة وحزم الخرائط', Icons.storage_rounded),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surfaceMuted,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.borderSubtle),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'حجم الذاكرة المؤقتة (Tiles Cache)',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 3),
Text(
_cacheSize,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.appleBlue,
),
),
],
),
TextButton.icon(
onPressed: _clearCache,
icon: const Icon(Icons.delete_sweep_rounded,
size: 18, color: AppColors.coralDanger),
label: const Text(
'مسح الذاكرة',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: AppColors.coralDanger,
),
),
style: TextButton.styleFrom(
backgroundColor:
AppColors.coralDanger.withValues(alpha: 0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
],
),
),
],
),
),
],
),
);
}
Widget _buildSectionHeader(String title, IconData icon) {
return Row(
children: [
Icon(icon, size: 16, color: AppColors.appleBlue),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
fontFamily: '.SF Pro Text',
),
),
],
);
}
Widget _buildSwitchTile({
required String title,
required String subtitle,
required bool value,
required ValueChanged<bool> onChanged,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: AppColors.surfaceMuted,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.borderSubtle),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
fontSize: 11,
height: 1.3,
color: AppColors.textSecondary,
),
),
],
),
),
Switch.adaptive(
value: value,
activeColor: AppColors.appleBlue,
onChanged: (v) {
HapticFeedback.lightImpact();
onChanged(v);
},
),
],
),
);
}
}
@@ -0,0 +1,389 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/constants/app_colors.dart';
import '../../../../logic/cubits/navigation/navigation_cubit.dart';
import '../../../../logic/cubits/navigation/navigation_state.dart';
class SiroDrawerWidget extends StatelessWidget {
final NavigationCubit cubit;
final NavigationState state;
final VoidCallback onOpenTouristLandmarks;
final VoidCallback onOpen3DStudio;
final VoidCallback onOpenLayers;
final VoidCallback onOpenSettings;
final VoidCallback onOpenUpdates;
final VoidCallback onOpenAbout;
final VoidCallback onStartReportHazard;
final VoidCallback onStartAddPlace;
const SiroDrawerWidget({
super.key,
required this.cubit,
required this.state,
required this.onOpenTouristLandmarks,
required this.onOpen3DStudio,
required this.onOpenLayers,
required this.onOpenSettings,
required this.onOpenUpdates,
required this.onOpenAbout,
required this.onStartReportHazard,
required this.onStartAddPlace,
});
@override
Widget build(BuildContext context) {
return Drawer(
backgroundColor: AppColors.pureWhite,
elevation: 16,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.horizontal(left: Radius.circular(28)),
),
child: SafeArea(
child: Column(
children: [
// ── Header: Luxury Siro Maps Identity ──
Container(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF1D1D1F), Color(0xFF2C2C2E)],
begin: Alignment.topRight,
end: Alignment.bottomLeft,
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.sovereignGold.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.sovereignGold.withValues(alpha: 0.4),
),
),
child: const Icon(
Icons.explore_rounded,
color: AppColors.sovereignGold,
size: 24,
),
),
// Online/Offline Sovereign Badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: state.isOnline
? AppColors.tacticalEmerald.withValues(alpha: 0.2)
: AppColors.sovereignGold.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: state.isOnline
? AppColors.tacticalEmerald.withValues(alpha: 0.4)
: AppColors.sovereignGold.withValues(alpha: 0.4),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: state.isOnline ? AppColors.tacticalEmerald : AppColors.sovereignGold,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
state.isOnline ? 'سحابي متصل' : 'توجيه سيادي محلي',
style: TextStyle(
fontSize: 10.5,
fontWeight: FontWeight.w700,
color: state.isOnline ? AppColors.tacticalEmerald : AppColors.sovereignGold,
fontFamily: '.SF Pro Text',
),
),
],
),
),
],
),
const SizedBox(height: 14),
const Text(
'سيرو ماب • Siro Maps',
style: TextStyle(
color: Colors.white,
fontSize: 19,
fontWeight: FontWeight.w800,
fontFamily: '.SF Pro Text',
letterSpacing: -0.3,
),
),
const SizedBox(height: 3),
const Text(
'منظومة الملاحة السيادية والاستوديو ثلاثي الأبعاد',
style: TextStyle(
color: Color(0xFF98989D),
fontSize: 11.5,
fontWeight: FontWeight.w500,
fontFamily: '.SF Pro Text',
),
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'🛡️ استضافة ذاتية 100% • حماية الهوامش الربحية',
style: TextStyle(
color: AppColors.sovereignGold,
fontSize: 10,
fontWeight: FontWeight.w700,
fontFamily: '.SF Pro Text',
),
),
),
],
),
),
// ── Drawer Menu Items ──
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
// ── Section 1: السياحة والاستكشاف 3D ──
_buildSectionTitle('السياحة والاستكشاف السينمائي'),
_buildDrawerTile(
icon: Icons.travel_explore_rounded,
color: const Color(0xFFD97706),
title: 'الخريطة والمعالم السياحية 3D',
subtitle: 'عجائب الأردن، المعالم الأثرية والمحميات',
onTap: () {
Navigator.of(context).pop();
onOpenTouristLandmarks();
},
),
_buildDrawerTile(
icon: Icons.view_in_ar_rounded,
color: const Color(0xFFEA580C),
title: 'استوديو التضاريس والسينما 3D',
subtitle: 'محاكاة الشمس، تضخيم الجبال، والدوران',
onTap: () {
Navigator.of(context).pop();
onOpen3DStudio();
},
),
_buildDrawerTile(
icon: Icons.layers_rounded,
color: AppColors.appleBlue,
title: 'طبقات الخريطة والأنماط',
subtitle: 'النمط التكتيكي، الأقمار الصناعية، والطبوغرافي',
onTap: () {
Navigator.of(context).pop();
onOpenLayers();
},
),
const SizedBox(height: 12),
const Divider(height: 1, color: AppColors.borderSubtle),
const SizedBox(height: 8),
// ── Section 2: مجتمع الطرق ──
_buildSectionTitle('المجتمع وحالة الطرق'),
_buildDrawerTile(
icon: Icons.warning_amber_rounded,
color: AppColors.coralDanger,
title: 'إبلاغ عن حالة طريق / مخاطر',
subtitle: 'حوادث، إغلاقات، وأعمال صيانة لحظية',
onTap: () {
Navigator.of(context).pop();
onStartReportHazard();
},
),
_buildDrawerTile(
icon: Icons.add_location_alt_rounded,
color: AppColors.appleBlue,
title: 'إضافة معلم أو منشأة جديدة',
subtitle: 'تثبيت موقع أو نشاط تجاري على الخريطة',
onTap: () {
Navigator.of(context).pop();
onStartAddPlace();
},
),
const SizedBox(height: 12),
const Divider(height: 1, color: AppColors.borderSubtle),
const SizedBox(height: 8),
// ── Section 3: الإعدادات والتحديثات ──
_buildSectionTitle('المنظومة والتحديثات'),
_buildDrawerTile(
icon: Icons.settings_suggest_rounded,
color: AppColors.textPrimary,
title: 'إعدادات المنظومة والملاحة',
subtitle: 'تفضيلات التوجيه، الرسوم، والذاكرة المؤقتة',
onTap: () {
Navigator.of(context).pop();
onOpenSettings();
},
),
_buildDrawerTile(
icon: Icons.system_update_alt_rounded,
color: AppColors.tacticalEmerald,
title: 'التحقق من تحديثات الخرائط',
subtitle: 'حزم البلاطات وقواعد بيانات التوجيه',
onTap: () {
Navigator.of(context).pop();
onOpenUpdates();
},
),
_buildDrawerTile(
icon: Icons.info_outline_rounded,
color: AppColors.appleBlue,
title: 'عن سيرو ماب والجوائز العالمية',
subtitle: 'البنية التحتية، الوفورات الاقتصادية، والتراخيص',
onTap: () {
Navigator.of(context).pop();
onOpenAbout();
},
),
],
),
),
// ── Footer: Version & Technical Footprint ──
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: AppColors.borderSubtle)),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Siro Maps Platform v1.2.0',
style: TextStyle(
fontSize: 10.5,
fontWeight: FontWeight.w600,
color: AppColors.textMuted,
fontFamily: '.SF Pro Text',
),
),
Text(
'Sovereignty Architecture',
style: TextStyle(
fontSize: 10.5,
fontWeight: FontWeight.w700,
color: AppColors.sovereignGold,
fontFamily: '.SF Pro Text',
),
),
],
),
),
],
),
),
);
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.fromLTRB(8, 10, 8, 6),
child: Text(
title,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
color: AppColors.textMuted,
fontFamily: '.SF Pro Text',
letterSpacing: -0.2,
),
),
);
}
Widget _buildDrawerTile({
required IconData icon,
required Color color,
required String title,
required String subtitle,
required VoidCallback onTap,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 6),
child: InkWell(
onTap: () {
HapticFeedback.lightImpact();
onTap();
},
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Row(
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withValues(alpha: 0.25), width: 0.8),
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
fontFamily: '.SF Pro Text',
),
),
const SizedBox(height: 2),
Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 10.5,
color: AppColors.textSecondary,
fontFamily: '.SF Pro Text',
),
),
],
),
),
const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: AppColors.textMuted),
],
),
),
),
);
}
}
@@ -0,0 +1,277 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/constants/app_colors.dart';
import '../../common/siro_toast.dart';
class SystemUpdatesSheet extends StatefulWidget {
const SystemUpdatesSheet({super.key});
@override
State<SystemUpdatesSheet> createState() => _SystemUpdatesSheetState();
}
class _SystemUpdatesSheetState extends State<SystemUpdatesSheet> {
bool _isChecking = false;
String _lastChecked = 'منذ دقيقة واحدة';
bool _isUpToDate = true;
Future<void> _checkForUpdates() async {
setState(() => _isChecking = true);
HapticFeedback.lightImpact();
await Future.delayed(const Duration(milliseconds: 1400));
if (mounted) {
setState(() {
_isChecking = false;
_lastChecked = 'الآن';
_isUpToDate = true;
});
SiroToast.showSuccess(
context,
'حزم الخرائط وقواعد بيانات التوجيه السيادية محدثة بالكامل لأحدث إصدار.',
title: 'المنظومة محدثة',
);
}
}
@override
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.75,
),
decoration: const BoxDecoration(
color: AppColors.pureWhite,
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
boxShadow: [
BoxShadow(
color: Color(0x33000000),
blurRadius: 30,
offset: Offset(0, -6),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Drag Handle
Center(
child: Container(
margin: const EdgeInsets.only(top: 12, bottom: 8),
width: 44,
height: 5,
decoration: BoxDecoration(
color: AppColors.borderSubtle,
borderRadius: BorderRadius.circular(3),
),
),
),
// Header
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.tacticalEmerald.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color:
AppColors.tacticalEmerald.withValues(alpha: 0.3)),
),
child: const Icon(Icons.system_update_alt_rounded,
color: AppColors.tacticalEmerald, size: 22),
),
const SizedBox(width: 14),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'تحديثات المنظومة وحزم الخرائط',
style: TextStyle(
fontSize: 16.5,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
fontFamily: '.SF Pro Text',
),
),
SizedBox(height: 2),
Text(
'متابعة حزم الخرائط غير المتصلة ومحركات التوجيه',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
fontFamily: '.SF Pro Text',
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close_rounded,
color: AppColors.textMuted),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
const Divider(height: 12, color: AppColors.borderSubtle),
// Updates Body
Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
child: Column(
children: [
// Status Box
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surfaceMuted,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: _isUpToDate
? AppColors.tacticalEmerald.withValues(alpha: 0.3)
: AppColors.sovereignGold.withValues(alpha: 0.3),
),
),
child: Column(
children: [
Row(
children: [
Icon(
_isUpToDate
? Icons.check_circle_rounded
: Icons.info_outline_rounded,
color: _isUpToDate
? AppColors.tacticalEmerald
: AppColors.sovereignGold,
size: 26,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_isUpToDate
? 'الخرائط والبيانات محدثة بالكامل'
: 'تحديث جديد متوفر',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 2),
Text(
'آخر فحص: $_lastChecked',
style: const TextStyle(
fontSize: 11,
color: AppColors.textMuted,
),
),
],
),
),
],
),
const SizedBox(height: 14),
const Divider(height: 1, color: AppColors.borderSubtle),
const SizedBox(height: 12),
_buildPackageRow(
'حزمة البلاطات الموجهة (Jordan Vector Tiles)',
'v2026.09 (أحدث إصدار)'),
const SizedBox(height: 8),
_buildPackageRow(
'محرك التوجيه السيادي (GraphHopper Graph)',
'v0.13.0 - مُحسَّن ومفهرس'),
const SizedBox(height: 8),
_buildPackageRow(
'بيانات الارتفاعات والتضاريس 3D (DEM & Contours)',
'نشط ومحمّل بالذاكرة'),
],
),
),
const SizedBox(height: 20),
// Check Button
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
onPressed: _isChecking ? null : _checkForUpdates,
icon: _isChecking
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white),
)
: const Icon(Icons.refresh_rounded,
size: 20, color: Colors.white),
label: Text(
_isChecking
? 'جاري التحقق من السيرفر...'
: 'البحث عن تحديثات الآن',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.appleBlue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
elevation: 0,
),
),
),
],
),
),
],
),
);
}
Widget _buildPackageRow(String name, String status) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
name,
style: const TextStyle(
fontSize: 11.5,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
decoration: BoxDecoration(
color: AppColors.tacticalEmerald.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
status,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
color: AppColors.tacticalEmerald),
),
),
],
);
}
}
@@ -0,0 +1,521 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import '../../../../core/constants/app_colors.dart';
import '../../../../data/models/tourist_landmark_model.dart';
import '../../../../logic/cubits/navigation/navigation_cubit.dart';
import '../../../../logic/cubits/navigation/navigation_state.dart';
import '../../common/siro_toast.dart';
class TouristLandmarksSheet extends StatefulWidget {
final NavigationCubit cubit;
final NavigationState state;
const TouristLandmarksSheet({
super.key,
required this.cubit,
required this.state,
});
@override
State<TouristLandmarksSheet> createState() => _TouristLandmarksSheetState();
}
class _TouristLandmarksSheetState extends State<TouristLandmarksSheet> {
String _selectedCategory = 'all';
String _searchQuery = '';
final TextEditingController _searchController = TextEditingController();
final List<Map<String, String>> _categories = const [
{'key': 'all', 'label': 'الكل 🌍'},
{'key': 'heritage', 'label': 'تراث وعجائب 🏛️'},
{'key': 'nature', 'label': 'طبيعة ومحميات 🌲'},
{'key': 'adventure', 'label': 'مغامرة وتخييم ⛺'},
{'key': 'religious', 'label': 'ديني وتاريخي ⛪'},
{'key': 'coastal', 'label': 'شواطئ وبحر ⛵'},
];
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final myLoc = widget.state.myLocation;
final landmarks = TouristLandmark.curatedJordanLandmarks.where((lm) {
final matchesCategory =
_selectedCategory == 'all' || lm.category == _selectedCategory;
final matchesSearch = _searchQuery.isEmpty ||
lm.nameAr.contains(_searchQuery) ||
lm.nameEn.toLowerCase().contains(_searchQuery.toLowerCase()) ||
lm.tag.contains(_searchQuery);
return matchesCategory && matchesSearch;
}).toList();
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.85,
),
decoration: const BoxDecoration(
color: AppColors.pureWhite,
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
boxShadow: [
BoxShadow(
color: Color(0x33000000),
blurRadius: 30,
offset: Offset(0, -6),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ── Sheet Drag Handle ──
Center(
child: Container(
margin: const EdgeInsets.only(top: 12, bottom: 8),
width: 44,
height: 5,
decoration: BoxDecoration(
color: AppColors.borderSubtle,
borderRadius: BorderRadius.circular(3),
),
),
),
// ── Header Title & Sovereignty Badge ──
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFD97706), Color(0xFFEA580C)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: const Color(0xFFD97706).withValues(alpha: 0.3),
blurRadius: 10,
offset: const Offset(0, 3),
),
],
),
child: const Icon(Icons.travel_explore_rounded,
color: Colors.white, size: 22),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'الخريطة والمعالم السياحية 3D',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
fontFamily: '.SF Pro Text',
),
),
const SizedBox(height: 2),
Text(
'استكشاف سينمائي ثلاثي الأبعاد لأعظم المعالم الأثرية والطبيعية',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
fontFamily: '.SF Pro Text',
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close_rounded,
color: AppColors.textMuted),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
// ── Search Field ──
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
child: Container(
decoration: BoxDecoration(
color: AppColors.surfaceMuted,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.borderSubtle),
),
child: TextField(
controller: _searchController,
onChanged: (val) {
setState(() {
_searchQuery = val.trim();
});
},
decoration: InputDecoration(
hintText: 'ابحث عن معلم، موقع أثري، أو محمية طبيعية...',
hintStyle: const TextStyle(
fontSize: 12.5,
color: AppColors.textMuted,
fontFamily: '.SF Pro Text',
),
prefixIcon: const Icon(Icons.search_rounded,
color: AppColors.textMuted, size: 20),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear_rounded,
color: AppColors.textMuted, size: 18),
onPressed: () {
_searchController.clear();
setState(() {
_searchQuery = '';
});
},
)
: null,
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
),
),
),
),
// ── Filter Categories Chips ──
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
child: Row(
children: _categories.map((cat) {
final isSelected = _selectedCategory == cat['key'];
return Padding(
padding: const EdgeInsets.only(left: 8),
child: FilterChip(
selected: isSelected,
showCheckmark: false,
label: Text(
cat['label']!,
style: TextStyle(
fontSize: 12,
fontWeight:
isSelected ? FontWeight.w700 : FontWeight.w600,
color:
isSelected ? Colors.white : AppColors.textPrimary,
fontFamily: '.SF Pro Text',
),
),
backgroundColor: AppColors.surfaceMuted,
selectedColor: const Color(0xFFD97706),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: isSelected
? const Color(0xFFD97706)
: AppColors.borderSubtle,
width: 1,
),
),
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
onSelected: (_) {
HapticFeedback.selectionClick();
setState(() {
_selectedCategory = cat['key']!;
});
},
),
);
}).toList(),
),
),
const Divider(height: 12, color: AppColors.borderSubtle),
// ── Landmarks List ──
Expanded(
child: landmarks.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.travel_explore_rounded,
size: 48,
color:
AppColors.textMuted.withValues(alpha: 0.5)),
const SizedBox(height: 10),
const Text(
'لا توجد معالم مطابقة للبحث',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 4),
const Text(
'جرب البحث باسم معلم آخر أو تغيير التصنيف المختار',
style: TextStyle(
fontSize: 12, color: AppColors.textMuted),
),
],
),
),
)
: ListView.separated(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
itemCount: landmarks.length,
separatorBuilder: (_, __) => const SizedBox(height: 14),
itemBuilder: (context, index) {
final lm = landmarks[index];
double? distKm;
if (myLoc != null) {
distKm = widget.cubit.locationService.calculateDistance(
myLoc,
lm.coordinates,
) /
1000.0;
}
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.pureWhite,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: lm.accentColor.withValues(alpha: 0.25),
width: 1.2,
),
boxShadow: const [
BoxShadow(
color: Color(0x0C000000),
blurRadius: 12,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header: Icon + Title + Tag + Altitude
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color:
lm.accentColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color:
lm.accentColor.withValues(alpha: 0.3),
),
),
child: Icon(lm.iconData,
color: lm.accentColor, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
lm.nameAr,
style: const TextStyle(
fontSize: 14.5,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
fontFamily: '.SF Pro Text',
),
),
const SizedBox(height: 3),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: lm.accentColor
.withValues(alpha: 0.1),
borderRadius:
BorderRadius.circular(6),
),
child: Text(
lm.tag,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
color: lm.accentColor,
),
),
),
if (distKm != null) ...[
const SizedBox(width: 6),
Text(
'• ${distKm.toStringAsFixed(1)} كم',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
],
],
),
],
),
),
// Elevation Badge
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.surfaceMuted,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.borderSubtle),
),
child: Column(
children: [
const Icon(Icons.terrain_rounded,
size: 14,
color: AppColors.textSecondary),
const SizedBox(height: 2),
Text(
'${lm.elevationMeters.toInt()} م',
style: const TextStyle(
fontSize: 10.5,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
],
),
),
],
),
const SizedBox(height: 10),
// Description
Text(
lm.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 12,
height: 1.4,
color: AppColors.textSecondary,
fontFamily: '.SF Pro Text',
),
),
const SizedBox(height: 14),
// Action Buttons: [ 🎬 استعراض سينمائي 3D ] + [ 🧭 توجيه المسار ]
Row(
children: [
// 3D Cinematic Fly-to & Orbit Button
Expanded(
child: SizedBox(
height: 42,
child: OutlinedButton.icon(
onPressed: () {
Navigator.of(context).pop();
widget.cubit
.startOrbitAroundLandmark(lm);
SiroToast.showInfo(
context,
'جاري الطيران السينمائي 3D نحو ${lm.nameAr}',
title: 'استعراض المعلم 360°',
);
},
icon: const Icon(Icons.view_in_ar_rounded,
size: 16),
label: const Text(
'استعراض 3D ودوران',
style: TextStyle(
fontSize: 11.5,
fontWeight: FontWeight.w700,
),
),
style: OutlinedButton.styleFrom(
foregroundColor: lm.accentColor,
side: BorderSide(
color: lm.accentColor, width: 1.2),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(12),
),
),
),
),
),
const SizedBox(width: 10),
// Route & Navigate Button
Expanded(
child: SizedBox(
height: 42,
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
widget.cubit.calculateRouteTo(
lm.coordinates,
title: lm.nameAr,
);
SiroToast.showSuccess(
context,
'جاري تخطيط المسار السيادي إلى ${lm.nameAr}',
title: 'تخطيط المسار',
);
},
icon: const Icon(Icons.directions_rounded,
size: 16, color: Colors.white),
label: const Text(
'توجيه المسار',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.appleBlue,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(12),
),
),
),
),
),
],
),
],
),
);
},
),
),
],
),
);
}
}