feat: add device authentication, place gates support, and PiP navigation features
This commit is contained in:
@@ -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> {
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user