import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:geolocator/geolocator.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/api_provider.dart'; import '../../models/post.dart'; import '../../models/local_intel.dart'; import '../../services/local_intel_service.dart'; import '../../theme/app_theme.dart'; import 'package:go_router/go_router.dart'; import 'beacon_bottom_sheet.dart'; import 'create_beacon_sheet.dart'; import 'widgets/intel_cards.dart'; import 'widgets/resources_sheet.dart'; class BeaconScreen extends ConsumerStatefulWidget { final LatLng? initialMapCenter; const BeaconScreen({super.key, this.initialMapCenter}); @override ConsumerState createState() => _BeaconScreenState(); } class _BeaconScreenState extends ConsumerState { final MapController _mapController = MapController(); final LocalIntelService _intelService = LocalIntelService(); List _beacons = []; bool _isLoading = false; bool _isLoadingLocation = false; late LatLng _mapCenter; LatLng? _userLocation; bool _locationPermissionGranted = false; double _currentZoom = 14.0; bool _suppressAutoCenterOnUser = false; // Map expansion state bool _isMapExpanded = false; // Local intel data LocalIntelData _intelData = LocalIntelData(); bool _isLoadingIntel = false; @override void initState() { super.initState(); _mapCenter = widget.initialMapCenter ?? const LatLng(37.7749, -122.4194); _suppressAutoCenterOnUser = widget.initialMapCenter != null; if (widget.initialMapCenter != null) { _loadBeacons(center: widget.initialMapCenter); } _checkLocationPermission(); } Future _checkLocationPermission() async { final status = await Permission.location.status; if (mounted) { setState(() { _locationPermissionGranted = status.isGranted; }); if (status.isGranted) { await _getCurrentLocation(forceCenter: !_suppressAutoCenterOnUser); await _loadBeacons(); } } } Future _requestLocationPermission() async { setState(() => _isLoadingLocation = true); try { final status = await Permission.location.request(); setState(() { _locationPermissionGranted = status.isGranted; }); if (status.isGranted) { await _getCurrentLocation(forceCenter: !_suppressAutoCenterOnUser); await _loadBeacons(); } else { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Location access is required to show nearby beacons.')), ); } } finally { if (mounted) setState(() => _isLoadingLocation = false); } } Future _getCurrentLocation({bool forceCenter = false}) async { if (!_locationPermissionGranted) return; setState(() => _isLoadingLocation = true); try { final position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, ); if (mounted) { setState(() { _userLocation = LatLng(position.latitude, position.longitude); if (forceCenter || !_suppressAutoCenterOnUser) { _mapController.move(_userLocation!, _currentZoom); _suppressAutoCenterOnUser = false; } }); // Fetch local intel when we get location _fetchLocalIntel(); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Could not get your location: $e')), ); } } finally { if (mounted) setState(() => _isLoadingLocation = false); } } Future _fetchLocalIntel() async { final location = _userLocation ?? _mapCenter; setState(() => _isLoadingIntel = true); try { final data = await _intelService.fetchAllIntel( location.latitude, location.longitude, ); if (mounted) { setState(() { _intelData = data; _isLoadingIntel = false; }); } } catch (e) { if (mounted) { setState(() { _intelData = LocalIntelData.withError('Failed to load intel'); _isLoadingIntel = false; }); } } } Future _loadBeacons({LatLng? center}) async { final target = center ?? _userLocation ?? _mapCenter; setState(() => _isLoading = true); try { final apiService = ref.read(apiServiceProvider); final beacons = await apiService.fetchNearbyBeacons( lat: target.latitude, long: target.longitude, radius: 16000, ); if (mounted) { setState(() { _beacons = beacons; _isLoading = false; }); } } catch (e) { if (mounted) { setState(() => _isLoading = false); } } } void _onMapPositionChanged(MapCamera camera, bool hasGesture) { _mapCenter = camera.center; _currentZoom = camera.zoom; if (hasGesture) { _loadBeacons(center: _mapCenter); } } void _onMarkerTap(Post post) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (context) => BeaconBottomSheet(post: post), ); } void _onCreateBeacon() { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) => CreateBeaconSheet( centerLat: _mapCenter.latitude, centerLong: _mapCenter.longitude, onBeaconCreated: (post) { setState(() { _beacons.add(post); }); _loadBeacons(); }, ), ); } void _toggleMapExpanded() { setState(() { _isMapExpanded = !_isMapExpanded; }); } void _showResourcesSheet() { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) => ResourcesSheet( resources: _intelData.resources, userLocation: _userLocation, ), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final accentColor = theme.primaryColor; // Show overlays for permissions/opt-in if (!_locationPermissionGranted) { return Scaffold( body: _buildLocationPermissionOverlay(context, accentColor), ); } // Full-screen map mode if (_isMapExpanded) { return Scaffold( body: Stack( children: [ _buildMap(), _buildTopBar(context, showCollapseButton: true), _buildBeaconFab(accentColor), ], ), ); } // Split-panel dashboard mode return Scaffold( body: Column( children: [ // Top 40%: Map Expanded( flex: 4, child: Stack( children: [ _buildMap(), _buildTopBar(context, showExpandButton: true), _buildCenterMarker(theme), ], ), ), // Bottom 60%: Intel Grid Expanded( flex: 6, child: Container( decoration: BoxDecoration( color: AppTheme.scaffoldBg, borderRadius: const BorderRadius.vertical( top: Radius.circular(24), ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 16, offset: const Offset(0, -4), ), ], ), child: _buildIntelGrid(), ), ), ], ), floatingActionButton: _buildBeaconFabButton(accentColor), ); } Widget _buildMap() { return FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: _mapCenter, initialZoom: _currentZoom, onPositionChanged: _onMapPositionChanged, minZoom: 3.0, maxZoom: 19.0, ), children: [ TileLayer( urlTemplate: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', subdomains: const ['a', 'b', 'c', 'd'], userAgentPackageName: 'com.sojorn.app', retinaMode: RetinaMode.isHighDensity(context), ), MarkerLayer( markers: [ ..._beacons.map((beacon) => _createMarker(beacon)), if (_locationPermissionGranted && _userLocation != null) _createUserLocationMarker(), ], ), ], ); } Widget _buildCenterMarker(ThemeData theme) { if (!_locationPermissionGranted) { return const SizedBox.shrink(); } return Center( child: IgnorePointer( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 48, height: 48, decoration: BoxDecoration( shape: BoxShape.circle, color: theme.primaryColor.withValues(alpha: 0.15), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.2), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Icon( Icons.place_rounded, color: theme.primaryColor, size: 32, ), ), ], ), ), ); } Widget _buildTopBar( BuildContext context, { bool showExpandButton = false, bool showCollapseButton = false, }) { return Positioned( top: 0, left: 0, right: 0, child: SafeArea( child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withValues(alpha: 0.7), Colors.transparent, ], ), ), child: Row( children: [ IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.arrow_back, color: Colors.white), style: IconButton.styleFrom( backgroundColor: Colors.black26, ), ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Beacon Network', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Colors.white, fontWeight: FontWeight.bold, ), ), Text( '${_beacons.length} beacons nearby', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white70, ), ), ], ), ), if (_isLoading || _isLoadingLocation) const Padding( padding: EdgeInsets.only(right: 8), child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ), ), if (showExpandButton) IconButton( onPressed: _toggleMapExpanded, icon: const Icon(Icons.fullscreen, color: Colors.white), tooltip: 'Expand Map', style: IconButton.styleFrom( backgroundColor: Colors.black26, ), ), if (showCollapseButton) IconButton( onPressed: _toggleMapExpanded, icon: const Icon(Icons.fullscreen_exit, color: Colors.white), tooltip: 'Show Dashboard', style: IconButton.styleFrom( backgroundColor: Colors.black26, ), ), IconButton( onPressed: _isLoadingLocation ? null : () => _getCurrentLocation(forceCenter: true), icon: const Icon(Icons.my_location, color: Colors.white), tooltip: 'My Location', style: IconButton.styleFrom( backgroundColor: Colors.black26, ), ), IconButton( onPressed: () { _loadBeacons(); _fetchLocalIntel(); }, icon: const Icon(Icons.refresh, color: Colors.white), tooltip: 'Refresh', style: IconButton.styleFrom( backgroundColor: Colors.black26, ), ), ], ), ), ), ); } Widget _buildIntelGrid() { return CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ // Header SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 12), child: Row( children: [ Icon(Icons.insights, color: AppTheme.navyBlue, size: 24), const SizedBox(width: 8), Text( 'Local Intel', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.navyBlue, ), ), const Spacer(), if (_isLoadingIntel) const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), ], ), ), ), // Intel Cards Grid SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16), sliver: SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 1.1, ), delegate: SliverChildListDelegate([ // Card 1: Conditions ConditionsCard( weather: _intelData.weather, isLoading: _isLoadingIntel, ), // Card 2: Hazards HazardsCard( hazards: _intelData.hazards, isLoading: _isLoadingIntel, ), // Card 3: Visibility VisibilityCard( visibility: _intelData.visibility, isLoading: _isLoadingIntel, ), // Card 4: Public Resources ResourcesCard( resourceCount: _intelData.resources.length, isLoading: _isLoadingIntel, onTap: _showResourcesSheet, ), ]), ), ), // Secure Chat Entry Point SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16), child: _buildSecureChatCard(), ), ), // Bottom padding const SliverToBoxAdapter( child: SizedBox(height: 80), ), ], ); } Widget _buildSecureChatCard() { return Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide(color: AppTheme.egyptianBlue, width: 1), ), child: InkWell( onTap: () { context.push('/secure-chat'); }, borderRadius: BorderRadius.circular(16), child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: AppTheme.ksuPurple.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.lock_outline, color: AppTheme.ksuPurple, size: 24, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Secure Messages', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.navyBlue, ), ), const SizedBox(height: 4), Text( 'End-to-end encrypted ยท Mutual follows only', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppTheme.textDisabled, ), ), ], ), ), Icon( Icons.arrow_forward_ios, color: AppTheme.egyptianBlue, size: 16, ), ], ), ), ), ); } Widget _buildBeaconFab(Color accentColor) { return Positioned( bottom: 24, right: 24, child: _buildBeaconFabButton(accentColor), ); } Widget _buildBeaconFabButton(Color accentColor) { return FloatingActionButton.extended( onPressed: _onCreateBeacon, label: const Text('Drop Beacon'), icon: const Icon(Icons.add_location), backgroundColor: accentColor, foregroundColor: Colors.white, ); } Widget _buildLocationPermissionOverlay( BuildContext context, Color accentColor) { return Container( color: Colors.black87, child: Center( child: Container( margin: const EdgeInsets.all(32), padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(16), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.location_on, size: 64, color: accentColor), const SizedBox(height: 24), Text( 'Allow location access', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), const SizedBox(height: 16), Text( 'We use your location to show nearby beacons and local intel.', style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center, ), const SizedBox(height: 24), ElevatedButton.icon( onPressed: _isLoadingLocation ? null : _requestLocationPermission, icon: _isLoadingLocation ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.location_on), label: const Text('Allow Location'), style: ElevatedButton.styleFrom( backgroundColor: accentColor, foregroundColor: Colors.white, ), ), ], ), ), ), ); } Marker _createMarker(Post post) { final typeColor = post.beaconType?.color ?? Colors.blue; final typeIcon = post.beaconType?.icon ?? Icons.location_on; final fallbackBase = _userLocation ?? _mapCenter; final markerPosition = (post.latitude != null && post.longitude != null) ? LatLng(post.latitude!, post.longitude!) : LatLng( fallbackBase.latitude + ((post.distanceMeters ?? 0) / 111000), fallbackBase.longitude + ((post.distanceMeters ?? 0) / 111000), ); return Marker( point: markerPosition, width: 48, height: 48, child: GestureDetector( onTap: () => _onMarkerTap(post), child: Container( decoration: BoxDecoration( shape: BoxShape.circle, color: typeColor, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.3), blurRadius: 8), ], ), child: Icon(typeIcon, color: Colors.white, size: 28), ), ), ); } Marker _createUserLocationMarker() { return Marker( point: _userLocation!, width: 40, height: 40, child: _PulsingLocationIndicator(), ); } } class _PulsingLocationIndicator extends StatefulWidget { @override State<_PulsingLocationIndicator> createState() => _PulsingLocationIndicatorState(); } class _PulsingLocationIndicatorState extends State<_PulsingLocationIndicator> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, )..repeat(reverse: false); _animation = Tween(begin: 0.4, end: 1.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeOut), ); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, builder: (context, child) { return Stack( alignment: Alignment.center, children: [ Container( width: 40, height: 40, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.blue.withValues(alpha: 0.3 * (1 - _animation.value)), border: Border.all( color: Colors.blue .withValues(alpha: 0.5 * (1 - _animation.value)), width: 2, ), ), ), Container( width: 16, height: 16, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.blue, border: Border.all( color: Colors.white, width: 3, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.3), blurRadius: 4, spreadRadius: 1, ), ], ), ), ], ); }, ); } }