feat: add device authentication, place gates support, and PiP navigation features

This commit is contained in:
Hamza-Ayed
2026-09-19 12:34:26 +03:00
parent 55830beee8
commit ce896df30a
86 changed files with 3414 additions and 341 deletions
@@ -0,0 +1,503 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/constants/app_colors.dart';
class AboutAwardsSheet extends StatelessWidget {
const AboutAwardsSheet({super.key});
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: 0.88,
minChildSize: 0.5,
maxChildSize: 0.96,
builder: (context, scrollController) {
return Container(
decoration: const BoxDecoration(
color: AppColors.canvasLight,
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
boxShadow: [
BoxShadow(
color: Color(0x33000000),
blurRadius: 30,
offset: Offset(0, -6),
),
],
),
child: Column(
children: [
// Top drag bar
Center(
child: Container(
margin: const EdgeInsets.only(top: 12, bottom: 8),
width: 44,
height: 4.5,
decoration: BoxDecoration(
color: AppColors.borderGlass,
borderRadius: BorderRadius.circular(3),
),
),
),
// Scrollable content
Expanded(
child: ListView(
controller: scrollController,
padding: const EdgeInsets.fromLTRB(20, 8, 20, 36),
children: [
// ── 1. HEADER EMBLEM & TITLE ──
Center(
child: Column(
children: [
Container(
width: 110,
height: 110,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.pureWhite,
border: Border.all(
color: AppColors.urukGold.withValues(alpha: 0.45),
width: 2,
),
boxShadow: [
BoxShadow(
color: AppColors.urukGold.withValues(alpha: 0.2),
blurRadius: 28,
offset: const Offset(0, 8),
),
],
),
padding: const EdgeInsets.all(6),
child: ClipOval(
child: Image.asset(
'assets/images/uruk_prize_logo.png',
fit: BoxFit.contain,
),
),
),
const SizedBox(height: 14),
Text(
'جائزة أوروك الدولية',
style: GoogleFonts.alexandria(
fontSize: 22,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 2),
Text(
'URUK INTERNATIONAL PRIZE',
style: GoogleFonts.alexandria(
fontSize: 12,
fontWeight: FontWeight.w700,
color: AppColors.urukGoldDark,
letterSpacing: 1.5,
),
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: AppColors.urukGoldLight,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.urukGoldBorder),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.workspace_premium_rounded,
size: 16,
color: AppColors.urukGoldDark,
),
const SizedBox(width: 6),
Text(
'تكريم التميز والسيادة التكنولوجية',
style: GoogleFonts.alexandria(
fontSize: 11.5,
fontWeight: FontWeight.w700,
color: AppColors.urukGoldDark,
),
),
],
),
),
],
),
),
const SizedBox(height: 24),
// ── 2. ORIGIN & GENESIS STORY ──
_buildSectionCard(
icon: Icons.history_edu_rounded,
iconColor: AppColors.urukGoldDark,
title: 'قصة المنظومة: من أوروك إلى سيادة البيانات',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'تستلهم جائزة أوروك الدولية إرثها من حضارة أوروك العظيمة — مهد أول تدوين للرموز والتخطيط العمراني في فجر الحضارة البشرية — لتكريم المشروعات التي تصنع فارقاً استراتيجياً في العالم العربي.',
style: GoogleFonts.alexandria(
fontSize: 12.5,
height: 1.65,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 10),
Text(
'من رحم هذا البرنامج وتتويجاً لتكريم جائزة أوروك، وُلدت منظومة «خرائط أوروك - Uruk Map» كبنية تحتية سيادية بديلة ومستقلة تماماً، مصممة لحماية السيادة المكانية وتوفير خرائط وملاحة ذكية مخصصة لمنطقة الشرق الأوسط وشمال أفريقيا دون الارتهان للشركات العالمية.',
style: GoogleFonts.alexandria(
fontSize: 12.5,
height: 1.65,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
],
),
),
const SizedBox(height: 16),
// ── 3. TECH ARCHITECT & FOUNDER ──
_buildSectionCard(
icon: Icons.person_pin_circle_rounded,
iconColor: AppColors.appleBlue,
title: 'القيادة التقنية والمعمارية للمنظومة',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.appleBlue.withValues(alpha: 0.1),
border: Border.all(color: AppColors.appleBlue.withValues(alpha: 0.3)),
),
child: const Icon(Icons.architecture_rounded, color: AppColors.appleBlue, size: 24),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'م. حمزة عايد | Hamza Ayed',
style: GoogleFonts.alexandria(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
Text(
'Founding Tech Architect & Mobility Strategist',
style: GoogleFonts.alexandria(
fontSize: 11,
fontWeight: FontWeight.w600,
color: AppColors.appleBlue,
),
),
],
),
),
],
),
const SizedBox(height: 12),
Text(
'خبير استراتيجي في حركية النقل الذكي وتأسيس منظومات البيانات الحساسة بخبرة قيادية تتجاوز 20 عاماً في إدارة العمليات والأنظمة الحرجة، والمؤسس التقني المشارك لمنصات انطلق (Intaleq)، تريبز (Tripz)، ومنظومة سيرو (Siro Platform).',
style: GoogleFonts.alexandria(
fontSize: 12,
height: 1.6,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
],
),
),
const SizedBox(height: 16),
// ── 4. ECONOMIC MOAT & UNIT ECONOMICS ──
_buildSectionCard(
icon: Icons.account_balance_wallet_rounded,
iconColor: AppColors.tacticalEmerald,
title: 'الأثر الاقتصادي والتوسع الإقليمي',
child: Column(
children: [
_buildStatRow(
metric: '0.30\$+',
metricLabel: 'وفر مباشر في كل رحلة وطلب',
desc: 'استبدال فواتير Google Maps الباهظة بنظام ذاتي الاستضافة والتحكم الكامل.',
color: AppColors.tacticalEmerald,
),
const Divider(height: 20, color: AppColors.borderSubtle),
_buildStatRow(
metric: 'MENA',
metricLabel: 'استهداف الأسواق المحرومة من خرائط موثوقة',
desc: 'توفير ملاحة ذكية وبنية خرائط مستقلة في العراق، السودان، اليمن، سوريا، وإيران.',
color: AppColors.urukGoldDark,
),
const Divider(height: 20, color: AppColors.borderSubtle),
_buildStatRow(
metric: '100%',
metricLabel: 'سيادة رقمية وعزل جغرافي',
desc: 'خوادم متجهات وتوجيه محلي (Self-Hosted OSRM + Vector Tiles) مستقلة عن أي قيود أو واجهات أجنبية.',
color: AppColors.appleBlue,
),
const Divider(height: 20, color: AppColors.borderSubtle),
_buildStatRow(
metric: 'OFFLINE',
metricLabel: 'ملاحة ذكية كاملة دون اتصال',
desc: 'مواصلة التوجيه والانعطاف حتى في مناطق انعدام التغطية الخلوية والصحراوية.',
color: AppColors.sovereignGold,
),
],
),
),
const SizedBox(height: 16),
// ── 5. URUK EDITION APP ICON SHOWCASE ──
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF131722),
borderRadius: BorderRadius.circular(22),
border: Border.all(color: AppColors.urukGoldBorder),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.18),
blurRadius: 18,
offset: const Offset(0, 6),
),
],
),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.urukGold.withValues(alpha: 0.4)),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(15),
child: Image.asset(
'assets/images/siro_uruk_logo.png',
fit: BoxFit.cover,
),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'شعار إصدار أوروك الخاص',
style: GoogleFonts.alexandria(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppColors.pureWhite,
),
),
const SizedBox(height: 4),
Text(
'دمج جناح تمثال أوروك الذهبي مع سهم الملاحة الفضائي وخطوط الارتفاع الطبوغرافية.',
style: GoogleFonts.alexandria(
fontSize: 10.5,
color: const Color(0xFFB0B5C0),
height: 1.4,
),
),
],
),
),
],
),
),
const SizedBox(height: 24),
// ── 6. ACTIONS & SHARE ──
ElevatedButton.icon(
onPressed: () {
Clipboard.setData(const ClipboardData(text: 'https://intaleqapp.com/hamza.html'));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'تم نسخ رابط الملف التنفيذي ودراسة المنظومة بنجاح',
style: TextStyle(fontSize: 12),
),
backgroundColor: AppColors.tacticalEmerald,
duration: Duration(seconds: 2),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.textPrimary,
foregroundColor: AppColors.pureWhite,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 0,
),
icon: const Icon(Icons.link_rounded, size: 18, color: AppColors.urukGold),
label: Text(
'نسخ رابط ملف المشروع ودراسة الحالة التنفيذية',
style: GoogleFonts.alexandria(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 10),
OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 13),
side: const BorderSide(color: AppColors.borderSubtle),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Text(
'إغلاق والعودة للخريطة',
style: GoogleFonts.alexandria(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
),
],
),
),
],
),
);
},
);
}
Widget _buildSectionCard({
required IconData icon,
required Color iconColor,
required String title,
required Widget child,
}) {
return Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: AppColors.pureWhite,
borderRadius: BorderRadius.circular(22),
border: Border.all(color: AppColors.borderSubtle),
boxShadow: const [
BoxShadow(
color: Color(0x0A000000),
blurRadius: 16,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: iconColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, size: 18, color: iconColor),
),
const SizedBox(width: 10),
Expanded(
child: Text(
title,
style: GoogleFonts.alexandria(
fontSize: 13.5,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
),
],
),
const SizedBox(height: 12),
child,
],
),
);
}
Widget _buildStatRow({
required String metric,
required String metricLabel,
required String desc,
required Color color,
}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
constraints: const BoxConstraints(minWidth: 70),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
metric,
style: GoogleFonts.alexandria(
fontSize: 14,
fontWeight: FontWeight.w800,
color: color,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
metricLabel,
style: GoogleFonts.alexandria(
fontSize: 12,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 2),
Text(
desc,
style: GoogleFonts.alexandria(
fontSize: 11,
color: AppColors.textSecondary,
height: 1.4,
),
),
],
),
),
],
);
}
}
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../../../core/constants/app_colors.dart';
import '../../../../core/services/pip_service.dart';
import '../../../../logic/cubits/navigation/navigation_state.dart';
class ActiveNavHudWidget extends StatelessWidget {
@@ -235,32 +236,39 @@ class ActiveNavHudWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text(
state.formattedRemainingDuration,
style: const TextStyle(
fontFamily: '.SF Pro Text',
fontSize: 22,
fontWeight: FontWeight.w800,
color: AppColors.tacticalEmerald,
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
state.formattedRemainingDuration,
style: const TextStyle(
fontFamily: '.SF Pro Text',
fontSize: 20,
fontWeight: FontWeight.w800,
color: AppColors.tacticalEmerald,
),
),
),
const SizedBox(width: 8),
Text(
'• ${state.formattedRemainingDistance}',
style: const TextStyle(
fontFamily: '.SF Pro Text',
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
const SizedBox(width: 6),
Text(
'• ${state.formattedRemainingDistance}',
style: const TextStyle(
fontFamily: '.SF Pro Text',
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
),
],
],
),
),
const SizedBox(height: 2),
Text(
'وصول متوقع: ${state.arrivalTime}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: '.SF Pro Text',
fontSize: 11,
@@ -273,30 +281,58 @@ class ActiveNavHudWidget extends StatelessWidget {
),
// Mute Button
IconButton(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
onPressed: onToggleMute,
icon: Icon(
state.isMuted ? Icons.volume_off_rounded : Icons.volume_up_rounded,
color: state.isMuted ? AppColors.textMuted : AppColors.appleBlue,
size: 24,
size: 22,
),
),
const SizedBox(width: 4),
// Vehicle Customizer
if (onOpenVehicleCustomizer != null)
if (onOpenVehicleCustomizer != null) ...[
IconButton(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
onPressed: onOpenVehicleCustomizer,
icon: const Icon(
Icons.directions_car_filled_rounded,
color: AppColors.appleBlue,
size: 22,
size: 21,
),
),
const SizedBox(width: 4),
],
// Picture-in-Picture Mode
IconButton(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
onPressed: () {
PipService.instance.enterPictureInPicture();
},
icon: const Icon(
Icons.picture_in_picture_alt_rounded,
color: AppColors.appleBlue,
size: 21,
),
tooltip: 'تصغير النافذة (Picture-in-Picture)',
),
const SizedBox(width: 4),
// Recenter Map
IconButton(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
onPressed: onRecenter,
icon: const Icon(
Icons.my_location_rounded,
color: AppColors.textSecondary,
size: 22,
size: 21,
),
),
const SizedBox(width: 6),
@@ -55,7 +55,7 @@ class _AddPlaceSheetState extends State<AddPlaceSheet> {
),
const SizedBox(height: 18),
const Text(
'إضافة مكان جديد إلى خرائط سيرو',
'إضافة مكان جديد إلى خرائط أوروك',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
),
@@ -7,6 +7,8 @@ class ExplorePanelWidget extends StatelessWidget {
const ExplorePanelWidget({super.key, required this.onCategorySelected});
static const List<Map<String, dynamic>> _categories = [
{'title': 'مولات', 'query': 'مول', 'icon': Icons.local_mall_rounded},
{'title': 'مستشفيات', 'query': 'مستشفى', 'icon': Icons.local_hospital_rounded},
{'title': 'مطاعم', 'query': 'مطعم', 'icon': Icons.restaurant_rounded},
{'title': 'كافيهات', 'query': 'مقهى', 'icon': Icons.local_cafe_rounded},
{'title': 'وقود', 'query': 'محطة وقود', 'icon': Icons.local_gas_station_rounded},
@@ -6,12 +6,14 @@ class LayerSelectorSheet extends StatelessWidget {
final MapThemeType currentTheme;
final ValueChanged<MapThemeType> onThemeChanged;
final VoidCallback? onOpenVehicleCustomizer;
final VoidCallback? onOpenAboutAwards;
const LayerSelectorSheet({
super.key,
required this.currentTheme,
required this.onThemeChanged,
this.onOpenVehicleCustomizer,
this.onOpenAboutAwards,
});
@override
@@ -67,9 +69,9 @@ class LayerSelectorSheet extends StatelessWidget {
],
),
if (onOpenVehicleCustomizer != null) ...[
const SizedBox(height: 20),
const SizedBox(height: 18),
const Divider(height: 1, color: AppColors.borderSubtle),
const SizedBox(height: 16),
const SizedBox(height: 14),
ListTile(
onTap: onOpenVehicleCustomizer,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
@@ -98,6 +100,45 @@ class LayerSelectorSheet extends StatelessWidget {
trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 14, color: AppColors.textMuted),
),
],
if (onOpenAboutAwards != null) ...[
const SizedBox(height: 10),
ListTile(
onTap: onOpenAboutAwards,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: AppColors.urukGoldBorder),
),
tileColor: AppColors.urukGoldLight,
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.pureWhite,
shape: BoxShape.circle,
border: Border.all(color: AppColors.urukGold.withValues(alpha: 0.4)),
),
child: ClipOval(
child: Image.asset(
'assets/images/uruk_prize_logo.png',
fit: BoxFit.contain,
),
),
),
title: const Text(
'جائزة أوروك الدولية واعتماد المنظومة',
style: TextStyle(
fontSize: 13.5,
fontWeight: FontWeight.w700,
color: AppColors.urukGoldDark,
),
),
subtitle: const Text(
'قصة نشأة المشروع، السيادة الرقمية، والمعمار التقني',
style: TextStyle(fontSize: 11, color: AppColors.textSecondary),
),
trailing: const Icon(Icons.workspace_premium_rounded, size: 20, color: AppColors.urukGoldDark),
),
],
],
),
);
@@ -0,0 +1,236 @@
import 'package:flutter/material.dart';
import '../../../../core/constants/app_colors.dart';
import '../../../../logic/cubits/navigation/navigation_state.dart';
/// Ultra-compact, luxury floating Navigation HUD tailored for Picture-in-Picture (PiP)
/// and In-App floating mini-navigation mode on iOS & Android.
class PipNavHudWidget extends StatelessWidget {
final NavigationState state;
final VoidCallback? onExpand;
final VoidCallback? onStopNavigation;
const PipNavHudWidget({
super.key,
required this.state,
this.onExpand,
this.onStopNavigation,
});
IconData _getManeuverIcon(int sign) {
switch (sign) {
case -3:
return Icons.turn_sharp_left_rounded;
case -2:
return Icons.turn_left_rounded;
case -1:
return Icons.turn_slight_left_rounded;
case 1:
return Icons.turn_slight_right_rounded;
case 2:
return Icons.turn_right_rounded;
case 3:
return Icons.turn_sharp_right_rounded;
case 4:
return Icons.flag_rounded;
case 6:
return Icons.roundabout_right_rounded;
case -7:
case 7:
return Icons.u_turn_left_rounded;
case 8:
return Icons.u_turn_right_rounded;
case 0:
default:
return Icons.straight_rounded;
}
}
@override
Widget build(BuildContext context) {
final int sign = state.currentManeuverModifier;
final String instruction = state.currentInstruction.isNotEmpty
? state.currentInstruction
: 'تابع السير على المسار';
final int speed = state.speed.clamp(0, 260).round();
return Material(
color: Colors.transparent,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
child: Container(
decoration: BoxDecoration(
color: const Color(0xF518181A), // Deep luxury iOS & Android dark glass
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withValues(alpha: 0.15), width: 1),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.45),
blurRadius: 14,
offset: const Offset(0, 4),
),
],
),
child: InkWell(
onTap: onExpand,
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ── 1. TOP TIER: MANEUVER BADGE + DISTANCE + INSTRUCTION + ACTIONS ──
Row(
children: [
// Turn Maneuver Icon Badge
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColors.tacticalEmerald,
Color(0xFF009624),
],
),
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: AppColors.tacticalEmerald.withValues(alpha: 0.4),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Icon(
_getManeuverIcon(sign),
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 8),
// Distance & Instruction
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Text(
state.formattedDistanceToStep,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w900,
color: Colors.white,
height: 1.1,
),
),
),
const SizedBox(height: 2),
Text(
instruction,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.white.withValues(alpha: 0.9),
),
),
],
),
),
const SizedBox(width: 6),
// Expand Button
IconButton(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
onPressed: onExpand,
icon: const Icon(
Icons.open_in_full_rounded,
color: AppColors.appleBlue,
size: 19,
),
tooltip: 'تكبير للشاشة الكاملة',
),
// Stop Navigation Button
if (onStopNavigation != null) ...[
const SizedBox(width: 4),
IconButton(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
onPressed: onStopNavigation,
icon: const Icon(
Icons.close_rounded,
color: AppColors.coralDanger,
size: 19,
),
tooltip: 'إنهاء الملاحة',
),
],
],
),
const SizedBox(height: 6),
// ── 2. BOTTOM TIER: TRIP ETA & SPEED BAR (OVERFLOW-PROOF) ──
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3.5),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
'${state.formattedRemainingDuration} • ${state.formattedRemainingDistance}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 10.5,
fontWeight: FontWeight.w700,
color: AppColors.textSecondary,
),
),
),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
decoration: BoxDecoration(
color: (speed > 100 ? AppColors.coralDanger : AppColors.tacticalEmerald)
.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'$speed كم/س',
style: TextStyle(
fontSize: 9.5,
fontWeight: FontWeight.w800,
color: speed > 100 ? AppColors.coralDanger : AppColors.tacticalEmerald,
),
),
),
],
),
),
],
),
),
),
),
),
),
);
}
}
@@ -0,0 +1,328 @@
import 'package:flutter/material.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../../../../core/constants/app_colors.dart';
import '../../../../core/services/location_service.dart';
import '../../../../data/models/place_gate.dart';
import '../../../../data/models/place_model.dart';
class PlaceGatesSheet extends StatelessWidget {
final PlaceModel place;
final LatLng? userLocation;
final Function(PlaceGate gate) onSelectGate;
final VoidCallback onSelectMainPlace;
const PlaceGatesSheet({
super.key,
required this.place,
this.userLocation,
required this.onSelectGate,
required this.onSelectMainPlace,
});
IconData _getGateIcon(PlaceGate gate) {
final name = gate.nameAr.toLowerCase();
if (name.contains('طوارئ') || name.contains('طوارئ')) {
return Icons.emergency_rounded;
}
if (name.contains('كارفور') || name.contains('سوق') || name.contains('تسوق')) {
return Icons.shopping_bag_rounded;
}
if (name.contains('زوار') || name.contains('مراجعين')) {
return Icons.people_alt_rounded;
}
if (name.contains('مواقف') || name.contains('كراج') || name.contains('سفلي')) {
return Icons.local_parking_rounded;
}
return Icons.door_sliding_rounded;
}
String? _getFormattedDistance(PlaceGate gate) {
if (userLocation == null) return null;
final distM = LocationService.instance.calculateDistance(
userLocation!,
LatLng(gate.latitude, gate.longitude),
);
if (distM >= 1000) {
return '${(distM / 1000).toStringAsFixed(1)} كم';
}
return '${distM.round()} م';
}
@override
Widget build(BuildContext context) {
final sortedGates = List<PlaceGate>.from(place.gates)
..sort((a, b) {
if (a.isMainGate && !b.isMainGate) return -1;
if (!a.isMainGate && b.isMainGate) return 1;
return 0;
});
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.65,
),
decoration: const BoxDecoration(
color: AppColors.pureWhite,
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
boxShadow: [
BoxShadow(
color: Color(0x24000000),
blurRadius: 32,
offset: Offset(0, -8),
),
],
),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Top Drag Handle
Center(
child: Container(
margin: const EdgeInsets.only(top: 12, bottom: 8),
width: 44,
height: 5,
decoration: BoxDecoration(
color: Colors.black12,
borderRadius: BorderRadius.circular(10),
),
),
),
// Header Section
Padding(
padding: const EdgeInsets.fromLTRB(20, 6, 20, 16),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.appleBlue.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.meeting_room_rounded,
color: AppColors.appleBlue,
size: 26,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
place.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColors.appleBlue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'${place.gates.length} بوابات',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: AppColors.appleBlue,
),
),
),
],
),
const SizedBox(height: 2),
const Text(
'اختر البوابة أو المدخل الأقرب لوجهتك المحددة',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
],
),
),
],
),
),
const Divider(height: 1, color: AppColors.borderSubtle),
// Gates List
Flexible(
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
shrinkWrap: true,
itemCount: sortedGates.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final gate = sortedGates[index];
final distStr = _getFormattedDistance(gate);
final isEmergency = gate.nameAr.contains('طوارئ');
return InkWell(
onTap: () {
Navigator.of(context).pop();
onSelectGate(gate);
},
borderRadius: BorderRadius.circular(18),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: gate.isMainGate
? AppColors.appleBlue.withValues(alpha: 0.05)
: AppColors.canvasLight,
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: gate.isMainGate
? AppColors.appleBlue.withValues(alpha: 0.3)
: AppColors.borderSubtle,
width: gate.isMainGate ? 1.5 : 1.0,
),
),
child: Row(
children: [
// Gate Icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isEmergency
? AppColors.coralDanger.withValues(alpha: 0.12)
: (gate.isMainGate
? AppColors.tacticalEmerald.withValues(alpha: 0.12)
: Colors.white),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
_getGateIcon(gate),
color: isEmergency
? AppColors.coralDanger
: (gate.isMainGate ? AppColors.tacticalEmerald : AppColors.textSecondary),
size: 20,
),
),
const SizedBox(width: 14),
// Gate Name
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
gate.nameAr,
style: TextStyle(
fontSize: 14,
fontWeight: gate.isMainGate ? FontWeight.w800 : FontWeight.w700,
color: isEmergency ? AppColors.coralDanger : AppColors.textPrimary,
),
),
),
if (gate.isMainGate) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppColors.tacticalEmerald.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'رئيسية',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
color: AppColors.tacticalEmerald,
),
),
),
],
],
),
if (gate.nameEn != null && gate.nameEn!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
gate.nameEn!,
style: const TextStyle(
fontSize: 11,
color: AppColors.textMuted,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
// Distance badge
if (distStr != null) ...[
Text(
distStr,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: AppColors.textSecondary,
),
),
const SizedBox(width: 8),
],
const Icon(
Icons.arrow_forward_ios_rounded,
size: 14,
color: AppColors.textMuted,
),
],
),
),
);
},
),
),
// General Destination Option
Padding(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 12),
child: TextButton.icon(
onPressed: () {
Navigator.of(context).pop();
onSelectMainPlace();
},
icon: const Icon(Icons.place_outlined, size: 18, color: AppColors.textSecondary),
label: const Text(
'التوجه إلى الموقع العام للمجمع',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
style: TextButton.styleFrom(
minimumSize: const Size(double.infinity, 44),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
),
),
],
),
),
);
}
}