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
+161 -23
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -5,15 +6,21 @@ import 'package:intaleq_maps/intaleq_maps.dart';
import '../../core/constants/api_constants.dart';
import '../../core/constants/app_colors.dart';
import '../../core/services/deep_link_service.dart';
import '../../core/services/pip_service.dart';
import '../../logic/cubits/navigation/navigation_cubit.dart';
import '../../logic/cubits/navigation/navigation_state.dart';
import '../../data/models/place_model.dart';
import 'widgets/search_bar_widget.dart';
import 'widgets/explore_panel_widget.dart';
import 'widgets/active_nav_hud_widget.dart';
import 'widgets/pip_nav_hud_widget.dart';
import 'widgets/place_gates_sheet.dart';
import 'widgets/layer_selector_sheet.dart';
import 'widgets/report_hazard_sheet.dart';
import 'widgets/add_place_sheet.dart';
import 'widgets/vehicle_customizer_sheet.dart';
import 'widgets/about_awards_sheet.dart';
class MapView extends StatefulWidget {
const MapView({super.key});
@@ -26,6 +33,7 @@ class _MapViewState extends State<MapView> {
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode();
bool _isSearchFocused = false;
StreamSubscription<DeepLinkTarget>? _deepLinkSub;
@override
void initState() {
@@ -34,6 +42,21 @@ class _MapViewState extends State<MapView> {
_searchFocusNode.addListener(() {
if (mounted) setState(() => _isSearchFocused = _searchFocusNode.hasFocus);
});
// Listen to deep links from external apps (geo:, siromaps://, google.navigation, etc.)
_deepLinkSub = DeepLinkService.instance.targetStream.listen((target) {
if (mounted) _handleDeepLinkTarget(target);
});
// Check if a deep link arrived while the app was cold starting
final initial = DeepLinkService.instance.initialTarget;
if (initial != null) {
DeepLinkService.instance.clearInitialTarget();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _handleDeepLinkTarget(initial);
});
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
print("📌 [MapView] PostFrameCallback: Triggering relockCameraToUser");
@@ -43,8 +66,26 @@ class _MapViewState extends State<MapView> {
});
}
Future<void> _handleDeepLinkTarget(DeepLinkTarget target) async {
print("🔗 [MapView] Handling DeepLinkTarget: $target");
final cubit = context.read<NavigationCubit>();
int retries = 0;
while (cubit.state.myLocation == null && retries < 15 && mounted) {
await Future.delayed(const Duration(milliseconds: 200));
retries++;
}
if (!mounted) return;
if (cubit.state.myLocation != null) {
await cubit.calculateRouteTo(target.destination, title: target.title);
if (target.autoStartNavigation && mounted) {
cubit.startNavigation();
}
}
}
@override
void dispose() {
_deepLinkSub?.cancel();
_searchController.dispose();
_searchFocusNode.dispose();
super.dispose();
@@ -66,6 +107,15 @@ class _MapViewState extends State<MapView> {
);
}
void _showAboutAwardsSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const AboutAwardsSheet(),
);
}
void _showLayerSelector(BuildContext context, NavigationCubit cubit, MapThemeType current) {
showModalBottomSheet(
context: context,
@@ -80,6 +130,10 @@ class _MapViewState extends State<MapView> {
Navigator.of(context).pop();
_showVehicleCustomizer(context, cubit, cubit.state);
},
onOpenAboutAwards: () {
Navigator.of(context).pop();
_showAboutAwardsSheet(context);
},
),
);
}
@@ -151,6 +205,32 @@ class _MapViewState extends State<MapView> {
});
}
void _showPlaceGatesSheet(BuildContext context, NavigationCubit cubit, PlaceModel place) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => PlaceGatesSheet(
place: place,
userLocation: cubit.state.myLocation,
onSelectGate: (gate) {
Navigator.of(context).pop();
cubit.calculateRouteTo(
LatLng(gate.latitude, gate.longitude),
title: '${place.name} - ${gate.nameAr}',
);
},
onSelectMainPlace: () {
Navigator.of(context).pop();
cubit.calculateRouteTo(
LatLng(place.latitude, place.longitude),
title: place.name,
);
},
),
);
}
@override
Widget build(BuildContext context) {
final cubit = context.read<NavigationCubit>();
@@ -212,21 +292,24 @@ class _MapViewState extends State<MapView> {
}
},
builder: (context, state) {
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: AppColors.canvasLight,
body: SizedBox.expand(
child: Stack(
fit: StackFit.expand,
children: [
// ── 1. REAL INTERACTIVE MAP ENGINE (Siro Real Tiles) ──
Positioned.fill(
child: _buildRealMapEngine(context, cubit, state),
),
return ValueListenableBuilder<bool>(
valueListenable: PipService.instance.isInPipMode,
builder: (context, isInPip, _) {
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: AppColors.canvasLight,
body: SizedBox.expand(
child: Stack(
fit: StackFit.expand,
children: [
// ── 1. REAL INTERACTIVE MAP ENGINE (Siro Real Tiles) ──
Positioned.fill(
child: _buildRealMapEngine(context, cubit, state),
),
// ── 2. TOP SEARCH BAR, OFFLINE BANNER & EXPLORE CHIPS ──
if (!state.isNavigating)
Positioned(
// ── 2. TOP SEARCH BAR, OFFLINE BANNER & EXPLORE CHIPS ──
if (!state.isNavigating && !isInPip)
Positioned(
top: 0,
left: 0,
right: 0,
@@ -426,6 +509,35 @@ class _MapViewState extends State<MapView> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (place.hasGates) ...[
Container(
margin: const EdgeInsets.only(bottom: 4),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppColors.appleBlue.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: AppColors.appleBlue.withValues(alpha: 0.3),
width: 0.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.meeting_room_rounded, size: 10, color: AppColors.appleBlue),
const SizedBox(width: 3),
Text(
'${place.gates.length} بوابات',
style: const TextStyle(
fontSize: 9,
fontWeight: FontWeight.w700,
color: AppColors.appleBlue,
),
),
],
),
),
],
if (distStr != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
@@ -460,10 +572,14 @@ class _MapViewState extends State<MapView> {
_searchController.clear();
cubit.clearSearch();
_searchFocusNode.unfocus();
cubit.calculateRouteTo(
LatLng(place.latitude, place.longitude),
title: place.name,
);
if (place.hasGates) {
_showPlaceGatesSheet(context, cubit, place);
} else {
cubit.calculateRouteTo(
LatLng(place.latitude, place.longitude),
title: place.name,
);
}
},
);
},
@@ -909,8 +1025,20 @@ class _MapViewState extends State<MapView> {
),
),
// ── 5. ACTIVE TURN-BY-TURN HUD & BANNER ──
if (state.isNavigating)
// ── 5. ACTIVE TURN-BY-TURN HUD & BANNER (OR PIP HUD) ──
if (state.isNavigating && isInPip)
Positioned(
top: 0,
left: 0,
right: 0,
child: PipNavHudWidget(
state: state,
onExpand: () => PipService.instance.exitPictureInPicture(),
onStopNavigation: cubit.stopNavigation,
),
),
if (state.isNavigating && !isInPip)
Positioned.fill(
child: SafeArea(
child: ActiveNavHudWidget(
@@ -925,7 +1053,7 @@ class _MapViewState extends State<MapView> {
),
// ── 5b. INTERACTIVE LOCATION PIN PICKER HUD (Add Place & Hazard) ──
if (state.isSelectingLocationOnMap) ...[
if (!isInPip && state.isSelectingLocationOnMap) ...[
// Centered Floating Target Pin
IgnorePointer(
child: Center(
@@ -1114,7 +1242,7 @@ class _MapViewState extends State<MapView> {
],
// ── 6. FLOATING ACTION BUTTONS (Right side, anchored to bottom) ──
if (!state.isNavigating && state.status != NavigationStatus.routePreview && !state.isSelectingLocationOnMap)
if (!isInPip && !state.isNavigating && state.status != NavigationStatus.routePreview && !state.isSelectingLocationOnMap)
Positioned(
right: 16,
bottom: 28,
@@ -1131,6 +1259,14 @@ class _MapViewState extends State<MapView> {
onTap: () => _showLayerSelector(context, cubit, state.mapTheme),
),
const SizedBox(height: 12),
// Uruk International Prize & Institutional Credentials Button
_buildFloatingCircle(
icon: Icons.workspace_premium_rounded,
color: AppColors.urukGoldDark,
tooltip: 'جائزة أوروك الدولية والسيادة المكانية',
onTap: () => _showAboutAwardsSheet(context),
),
const SizedBox(height: 12),
// Add Place Button
_buildFloatingCircle(
icon: Icons.add_location_alt_rounded,
@@ -1160,7 +1296,7 @@ class _MapViewState extends State<MapView> {
),
// ── 6b. LIVE FLOATING SPEEDOMETER (Bottom left, when driving) ──
if (!state.isNavigating && state.status != NavigationStatus.routePreview && state.speed > 3.0)
if (!isInPip && !state.isNavigating && state.status != NavigationStatus.routePreview && state.speed > 3.0)
Positioned(
left: 16,
bottom: 32,
@@ -1225,6 +1361,8 @@ class _MapViewState extends State<MapView> {
),
),
);
},
);
},
);
}