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:
@@ -0,0 +1,197 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../core/constants/app_colors.dart';
|
||||
|
||||
enum SiroToastType {
|
||||
success,
|
||||
error,
|
||||
info,
|
||||
warning,
|
||||
}
|
||||
|
||||
class SiroToast {
|
||||
SiroToast._();
|
||||
|
||||
static void showSuccess(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
String? title,
|
||||
Duration duration = const Duration(seconds: 4),
|
||||
}) {
|
||||
show(
|
||||
context,
|
||||
message: message,
|
||||
title: title,
|
||||
type: SiroToastType.success,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
static void showError(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
String? title,
|
||||
Duration duration = const Duration(seconds: 4),
|
||||
}) {
|
||||
show(
|
||||
context,
|
||||
message: message,
|
||||
title: title,
|
||||
type: SiroToastType.error,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
static void showInfo(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
String? title,
|
||||
Duration duration = const Duration(seconds: 4),
|
||||
}) {
|
||||
show(
|
||||
context,
|
||||
message: message,
|
||||
title: title,
|
||||
type: SiroToastType.info,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
static void showWarning(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
String? title,
|
||||
Duration duration = const Duration(seconds: 4),
|
||||
}) {
|
||||
show(
|
||||
context,
|
||||
message: message,
|
||||
title: title,
|
||||
type: SiroToastType.warning,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
String? title,
|
||||
SiroToastType type = SiroToastType.info,
|
||||
Duration duration = const Duration(seconds: 4),
|
||||
}) {
|
||||
// Trigger tactile haptic feedback
|
||||
switch (type) {
|
||||
case SiroToastType.success:
|
||||
HapticFeedback.lightImpact();
|
||||
break;
|
||||
case SiroToastType.error:
|
||||
HapticFeedback.heavyImpact();
|
||||
break;
|
||||
case SiroToastType.warning:
|
||||
HapticFeedback.mediumImpact();
|
||||
break;
|
||||
case SiroToastType.info:
|
||||
HapticFeedback.selectionClick();
|
||||
break;
|
||||
}
|
||||
|
||||
Color accentColor;
|
||||
IconData iconData;
|
||||
|
||||
switch (type) {
|
||||
case SiroToastType.success:
|
||||
accentColor = AppColors.tacticalEmerald;
|
||||
iconData = Icons.check_circle_rounded;
|
||||
break;
|
||||
case SiroToastType.error:
|
||||
accentColor = AppColors.coralDanger;
|
||||
iconData = Icons.error_rounded;
|
||||
break;
|
||||
case SiroToastType.warning:
|
||||
accentColor = AppColors.sovereignGold;
|
||||
iconData = Icons.warning_amber_rounded;
|
||||
break;
|
||||
case SiroToastType.info:
|
||||
accentColor = AppColors.appleBlue;
|
||||
iconData = Icons.info_rounded;
|
||||
break;
|
||||
}
|
||||
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.hideCurrentSnackBar();
|
||||
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
elevation: 10,
|
||||
margin: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).size.height * 0.11,
|
||||
left: 18,
|
||||
right: 18,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
side: BorderSide(
|
||||
color: accentColor.withValues(alpha: 0.35),
|
||||
width: 1.2,
|
||||
),
|
||||
),
|
||||
backgroundColor: const Color(0xFF1D1D1F).withValues(alpha: 0.95),
|
||||
duration: duration,
|
||||
content: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(7),
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor.withValues(alpha: 0.18),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: accentColor.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
iconData,
|
||||
color: accentColor,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null && title.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: accentColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: '.SF Pro Text',
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.3,
|
||||
fontFamily: '.SF Pro Text',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user