diff --git a/bash.exe.stackdump b/bash.exe.stackdump index 146949e..36e69e6 100644 --- a/bash.exe.stackdump +++ b/bash.exe.stackdump @@ -1,11 +1,31 @@ Stack trace: Frame Function Args 0007FFFFBBD0 00021005FE8E (000210285F68, 00021026AB6E, 0007FFFFBBD0, 0007FFFFAAD0) msys-2.0.dll+0x1FE8E -0007FFFFBBD0 0002100467F9 (000000000000, 000000000000, 000000000000, 0007FFFFBEA8) msys-2.0.dll+0x67F9 -0007FFFFBBD0 000210046832 (000210286019, 0007FFFFBA88, 0007FFFFBBD0, 000000000000) msys-2.0.dll+0x6832 +0007FFFFBBD0 0002100467F9 (000000000000, 000000000000, 000210059F6C, 0007FFFFBEA8) msys-2.0.dll+0x67F9 +0007FFFFBBD0 000210046832 (0002102860C8, 0007FFFFBA88, 0007FFFFBBD0, 0002102684E0) msys-2.0.dll+0x6832 0007FFFFBBD0 000210068CF6 (000000000000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x28CF6 -0007FFFFBBD0 000210068E24 (0007FFFFBBE0, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x28E24 -0007FFFFBEB0 00021006A225 (0007FFFFBBE0, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x2A225 +0007FFFFBBD0 000210068EFF (0007FFFFBBE0, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x28EFF +0007FFFFBEB0 00021006A225 (000000004000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x2A225 +0007FFFFBEB0 00021006A4A9 (000000000000, 0007FFFFBF58, 0007FFFFC134, 0000FFFFFFFF) msys-2.0.dll+0x2A4A9 +0007FFFFBEB0 000210193F2B (000000000000, 0007FFFFBF58, 0007FFFFC134, 0000FFFFFFFF) msys-2.0.dll+0x153F2B +0007FFFFBEB0 00010042DB65 (000A00000004, 0007FFFFC144, 00010040DD62, 000000000010) bash.exe+0x2DB65 +0000FFFFFFFF 00010043C4F8 (0000000000A0, 000A00000000, 000A0018A870, 000A00089D10) bash.exe+0x3C4F8 +000000000070 00010043E6BE (000A0018A290, 000A00000001, 000210178DD2, 0007FFFFC140) bash.exe+0x3E6BE +000000000070 000100441B06 (000700000001, 000A00000000, 0007FFFFC230, 000000000000) bash.exe+0x41B06 +000000000070 000100441D36 (000200000000, 000A00000000, 000000000000, 000000000000) bash.exe+0x41D36 +000A00051D20 000100444B1F (0000000000AD, 000A00119520, 000000000030, 0000000000AD) bash.exe+0x44B1F +000A00051D20 0001004151FF (000A000EB520, 0000000C8664, 8080808080808080, 0001004F6EF7) bash.exe+0x151FF +000A000EB520 00010041561E (000000000001, 000000000001, 000210178DD2, 000200000000) bash.exe+0x1561E +0000FFFFFFFF 00010041792C (000A00193070, 000000000000, 000A00193070, 000A0018A7A0) bash.exe+0x1792C +0000000000AD 00010041AC6A (000A00061DF0, 000210268720, 000100620700, 000000000001) bash.exe+0x1AC6A +0000000000AD 0001004180FB (0007FFFFC630, 000A000297D0, 0002100AC638, 000A0018A6C0) bash.exe+0x180FB +000000000001 00010046F3A5 (000A00020740, 000000000414, 0000CE000000, 0000000000BE) bash.exe+0x6F3A5 +000A00029880 00010046E127 (000000000009, 000A000230E0, 000000000000, 0007FFFFCC0F) bash.exe+0x6E127 +000000000000 00010046E2D5 (0002102ADAC0, 0002102686E0, 000000000000, 000000000005) bash.exe+0x6E2D5 +000000000000 0001004EA48C (000A00002990, 000A00000160, 000000000000, 000700000000) bash.exe+0xEA48C +0007FFFFCD30 000210047F01 (000000000000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x7F01 +000000000000 000210045AC3 (000000000000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x5AC3 +0007FFFFFFF0 000210045B74 (000000000000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x5B74 End of stack trace Loaded modules: 000100400000 bash.exe @@ -17,8 +37,8 @@ Loaded modules: 7FFF4C690000 GDI32.dll 7FFF4AF30000 gdi32full.dll 7FFF4AE90000 msvcp_win.dll -7FFF4B510000 ucrtbase.dll 000210040000 msys-2.0.dll +7FFF4B510000 ucrtbase.dll 7FFF4CC90000 advapi32.dll 7FFF4B610000 msvcrt.dll 7FFF4B850000 sechost.dll diff --git a/lib/features/disaster_alerts/pages/evacuation_screen.dart b/lib/features/disaster_alerts/pages/evacuation_screen.dart index 076d84a..7f97946 100644 --- a/lib/features/disaster_alerts/pages/evacuation_screen.dart +++ b/lib/features/disaster_alerts/pages/evacuation_screen.dart @@ -18,6 +18,7 @@ import 'package:disaster_management/shared/widgets/chat_assistance.dart'; import '../models/place_type.dart'; import 'dart:typed_data'; import 'dart:ui' as ui; + //cicd checks code ql fi class EvacuationScreen extends StatefulWidget { const EvacuationScreen({super.key}); @@ -43,20 +44,20 @@ class _EvacuationScreenState extends State { final ScrollController _placesScrollController = ScrollController(); List _allPlaces = []; bool _isMapExpanded = false; - + // Helper methods for route information String _calculateRouteDistance() { // In a real app, this would calculate the actual distance // For now, return a placeholder value return '3.2'; } - + String _calculateRouteDuration() { // In a real app, this would calculate the actual duration // For now, return a placeholder value return '12'; } - + void _clearRoute() { setState(() { _polylines.clear(); @@ -115,7 +116,7 @@ class _EvacuationScreenState extends State { } } -Future _fetchNearbyPlaces() async { + Future _fetchNearbyPlaces() async { if (_currentPosition == null) return; setState(() { @@ -201,13 +202,13 @@ Future _fetchNearbyPlaces() async { } } - // New method to generate enhanced markers + // New method to generate enhanced markers Future> _generateEnhancedMarkers({ required List places, required LatLng currentPosition, }) async { final Set markers = {}; - + // Add current location marker final currentLocationMarker = await _createCustomMarker( id: 'current_location', @@ -219,7 +220,7 @@ Future _fetchNearbyPlaces() async { isCurrentLocation: true, ); markers.add(currentLocationMarker); - + // Add place markers for (var place in places) { final placeMarker = await _createCustomMarker( @@ -234,7 +235,7 @@ Future _fetchNearbyPlaces() async { ); markers.add(placeMarker); } - + return markers; } @@ -257,7 +258,7 @@ Future _fetchNearbyPlaces() async { rating: rating, isCurrentLocation: isCurrentLocation, ); - + return Marker( markerId: MarkerId(id), position: position, @@ -270,7 +271,7 @@ Future _fetchNearbyPlaces() async { zIndex: isCurrentLocation ? 2 : 1, // Current location appears on top ); } - + // Create custom marker bitmap Future _createMarkerBitmap({ required IconData iconData, @@ -281,14 +282,14 @@ Future _fetchNearbyPlaces() async { // For simplicity, we'll use default markers with custom hues for now // In a production app, you would use a custom widget and RepaintBoundary // to create truly custom markers with ratings, etc. - + if (isCurrentLocation) { return BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure); } - + // Use different hues based on place type or rating double hue = BitmapDescriptor.hueRed; - + switch (_selectedPlaceType) { case PlaceType.hospital: hue = BitmapDescriptor.hueRed; @@ -305,7 +306,7 @@ Future _fetchNearbyPlaces() async { default: hue = BitmapDescriptor.hueRose; } - + return BitmapDescriptor.defaultMarkerWithHue(hue); } @@ -341,7 +342,7 @@ Future _fetchNearbyPlaces() async { }, ), ), - + // Back button Positioned( top: MediaQuery.of(context).padding.top + 16, @@ -364,7 +365,7 @@ Future _fetchNearbyPlaces() async { ), ), ), - + // Add a floating route info panel at the bottom if (_polylines.isNotEmpty) Positioned( @@ -392,7 +393,8 @@ Future _fetchNearbyPlaces() async { Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: EvacuationColors.primaryColor.withOpacity(0.1), + color: EvacuationColors.primaryColor + .withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -451,7 +453,8 @@ Future _fetchNearbyPlaces() async { style: ElevatedButton.styleFrom( backgroundColor: EvacuationColors.primaryColor, foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), + padding: + const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -468,7 +471,7 @@ Future _fetchNearbyPlaces() async { ), ); } - + // Regular view with small map return Stack( children: [ @@ -506,7 +509,7 @@ Future _fetchNearbyPlaces() async { selectedPlaceType: _selectedPlaceType, onTypeSelected: (type) { setState(() => _selectedPlaceType = type); - _fetchNearbyPlaces(); + _fetchNearbyPlaces(); //this component is component which we scroll and change type to hosptials ,polic stations etc }, ), const SizedBox(height: 24), @@ -658,7 +661,7 @@ Future _fetchNearbyPlaces() async { Widget _buildPlacesList() { return SliverPadding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + padding: const EdgeInsets.fromLTRB(12, 0, 12, 16), sliver: _displayedPlaces.isEmpty ? SliverFillRemaining( child: _isLoading ? _buildLoadingIndicator() : _buildEmptyState()) @@ -819,127 +822,107 @@ Future _fetchNearbyPlaces() async { final screenWidth = MediaQuery.of(context).size.width; final isSmallScreen = screenWidth < 360; + // Calculate safe padding for extremely small screens + final double safePadding = isSmallScreen ? 8 : 12; + final double iconContainerSize = isSmallScreen ? 32 : 40; + final double iconSize = isSmallScreen ? 16 : 20; + final double fontSize = isSmallScreen ? 12 : 14; + return AnimatedContainer( duration: const Duration(milliseconds: 300), - margin: const EdgeInsets.only(bottom: 16), + margin: const EdgeInsets.only(bottom: 12), child: Material( color: EvacuationColors.cardBackground, - borderRadius: BorderRadius.circular(24), - elevation: 3, - shadowColor: EvacuationColors.shadowColor.withOpacity(0.3), + borderRadius: BorderRadius.circular(16), + elevation: 2, + shadowColor: EvacuationColors.shadowColor.withOpacity(0.2), child: InkWell( onTap: () => _showRouteToPlace(place), - borderRadius: BorderRadius.circular(24), - child: Container( - padding: EdgeInsets.all(isSmallScreen ? 12 : 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - border: Border.all( - color: EvacuationColors.borderColor.withOpacity(0.7), - width: 1.5, - ), - gradient: LinearGradient( - colors: [ - EvacuationColors.cardBackground, - EvacuationColors.cardBackground.withOpacity(0.95), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: EdgeInsets.all(safePadding), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - padding: EdgeInsets.all(isSmallScreen ? 8 : 12), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - EvacuationColors.primaryColor.withOpacity(0.2), - EvacuationColors.accentColor.withOpacity(0.2), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, + // Icon container - fixed size to prevent overflow + SizedBox( + width: iconContainerSize, + height: iconContainerSize, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: EvacuationColors.primaryColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + placeIcon, + color: EvacuationColors.primaryColor, + size: iconSize, ), - borderRadius: BorderRadius.circular(18), - boxShadow: [ - BoxShadow( - color: EvacuationColors.primaryColor.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Icon( - placeIcon, - color: EvacuationColors.primaryColor, - size: isSmallScreen ? 18 : 22, ), ), - SizedBox(width: isSmallScreen ? 8 : 14), + + // Spacing + SizedBox(width: safePadding / 2), + + // Content - use Expanded to prevent overflow Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ + // Name with flexible width Text( place.name, style: GoogleFonts.inter( color: EvacuationColors.textColor, - fontSize: isSmallScreen ? 13 : 15, + fontSize: fontSize, fontWeight: FontWeight.w600, height: 1.2, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), - SizedBox(height: isSmallScreen ? 4 : 8), - Wrap( - spacing: isSmallScreen ? 4 : 6, - runSpacing: isSmallScreen ? 4 : 6, - children: [ - if (place.rating != null) - _buildCompactInfoChip( - icon: Icons.star_rounded, - text: place.rating!.toString(), - color: const Color(0xFFFB923C), + + SizedBox(height: safePadding / 2), + + // Info chips - use SingleChildScrollView to prevent overflow + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (place.rating != null) + _buildSimpleInfoChip( + icon: Icons.star_rounded, + text: place.rating!.toString(), + color: const Color(0xFFFB923C), + isSmallScreen: isSmallScreen, + ), + SizedBox(width: safePadding / 2), + _buildSimpleInfoChip( + icon: Icons.location_on_rounded, + text: place.vicinity, + color: EvacuationColors.primaryColor, isSmallScreen: isSmallScreen, - maxWidth: - screenWidth * (isSmallScreen ? 0.15 : 0.2), ), - _buildCompactInfoChip( - icon: Icons.location_on_rounded, - text: place.vicinity, - color: EvacuationColors.primaryColor, - isSmallScreen: isSmallScreen, - maxWidth: - screenWidth * (isSmallScreen ? 0.35 : 0.45), - ), - ], + ], + ), ), ], ), ), - SizedBox(width: isSmallScreen ? 4 : 8), - Hero( - tag: 'arrow_${place.placeId}', - child: Container( - padding: EdgeInsets.all(isSmallScreen ? 6 : 8), - decoration: BoxDecoration( - color: EvacuationColors.primaryColor.withOpacity(0.15), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: EvacuationColors.primaryColor.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 1), - ), - ], - ), - child: Icon( - Icons.arrow_forward_ios_rounded, - color: EvacuationColors.primaryColor, - size: isSmallScreen ? 10 : 14, - ), + + // Chevron icon - fixed size to prevent overflow + Container( + margin: EdgeInsets.only(left: safePadding / 2), + width: iconSize, + height: iconSize, + alignment: Alignment.center, + child: Icon( + Icons.chevron_right, + color: EvacuationColors.primaryColor, + size: iconSize, ), ), ], @@ -950,24 +933,24 @@ Future _fetchNearbyPlaces() async { ); } - // More compact info chip for all screen sizes - Widget _buildCompactInfoChip({ + // Simple info chip without wrapping and with flexible width + Widget _buildSimpleInfoChip({ required IconData icon, required String text, required Color color, required bool isSmallScreen, - required double maxWidth, }) { + final double chipFontSize = isSmallScreen ? 10 : 12; + final double chipIconSize = isSmallScreen ? 10 : 12; + return Container( padding: EdgeInsets.symmetric( - horizontal: isSmallScreen ? 6 : 8, vertical: isSmallScreen ? 3 : 4), + horizontal: 6, + vertical: 2, + ), decoration: BoxDecoration( - color: color.withOpacity(0.12), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: color.withOpacity(0.2), - width: 1, - ), + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -975,22 +958,33 @@ Future _fetchNearbyPlaces() async { Icon( icon, color: color, - size: isSmallScreen ? 10 : 14, + size: chipIconSize, ), - SizedBox(width: isSmallScreen ? 3 : 4), - ConstrainedBox( - constraints: BoxConstraints(maxWidth: maxWidth), - child: Text( + const SizedBox(width: 2), + // For ratings, show full text + if (text.length <= 3) + Text( text, style: GoogleFonts.inter( - color: EvacuationColors.textColor, - fontSize: isSmallScreen ? 10 : 12, + fontSize: chipFontSize, fontWeight: FontWeight.w500, + color: EvacuationColors.textColor, + ), + ) + // For longer texts like vicinity, use flexible width with ellipsis + else + Flexible( + child: Text( + text, + style: GoogleFonts.inter( + fontSize: chipFontSize, + fontWeight: FontWeight.w500, + color: EvacuationColors.textColor, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), - ), ], ), ); @@ -1272,7 +1266,7 @@ Future _fetchNearbyPlaces() async { _polylines.clear(); _polylines.add(routePolyline); }); - + // If map is expanded, show the route info panel if (_isMapExpanded) { // Already showing the route info panel via the if condition in the UI @@ -1280,7 +1274,7 @@ Future _fetchNearbyPlaces() async { // Consider expanding the map to show the route _toggleMapExpansion(); } - + mapController.animateCamera( CameraUpdate.newLatLngBounds( MapBoundsCalculator.getRouteLatLngBounds(polylinePoints), diff --git a/lib/features/disaster_alerts/widgets/SideNavigation/nav_footer.dart b/lib/features/disaster_alerts/widgets/SideNavigation/nav_footer.dart index 7580831..ed6d57c 100644 --- a/lib/features/disaster_alerts/widgets/SideNavigation/nav_footer.dart +++ b/lib/features/disaster_alerts/widgets/SideNavigation/nav_footer.dart @@ -3,65 +3,47 @@ import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../constants/colors.dart'; -class NavFooter extends StatefulWidget { +class NavFooter extends StatelessWidget { final VoidCallback? onLogoutTap; + final bool isSmallScreen; const NavFooter({ Key? key, this.onLogoutTap, + this.isSmallScreen = false, }) : super(key: key); - @override - State createState() => _NavFooterState(); -} - -class _NavFooterState extends State with SingleTickerProviderStateMixin { - late AnimationController _hoverController; - bool _isHovering = false; - - @override - void initState() { - super.initState(); - _hoverController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 200), - ); - } - - @override - void dispose() { - _hoverController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { + final double padding = isSmallScreen ? 12.0 : 16.0; + return Container( padding: EdgeInsets.only( - left: 20, - right: 20, - bottom: MediaQuery.of(context).padding.bottom + 16, - top: 16, + left: padding, + right: padding, + bottom: + MediaQuery.of(context).padding.bottom + (isSmallScreen ? 8 : 12), + top: isSmallScreen ? 8 : 12, ), decoration: BoxDecoration( color: EvacuationColors.cardBackground, boxShadow: [ BoxShadow( color: EvacuationColors.shadowColor.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, -5), + blurRadius: 6, + offset: const Offset(0, -1), ), ], borderRadius: const BorderRadius.only( - topLeft: Radius.circular(24), - topRight: Radius.circular(24), + topLeft: Radius.circular(20), + topRight: Radius.circular(20), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ _buildAppVersion(), - const SizedBox(height: 16), + SizedBox(height: isSmallScreen ? 10 : 12), _buildLogoutButton(), ], ), @@ -69,86 +51,65 @@ class _NavFooterState extends State with SingleTickerProviderStateMix } Widget _buildLogoutButton() { - return MouseRegion( - onEnter: (_) { - setState(() { - _isHovering = true; - }); - _hoverController.forward(); - }, - onExit: (_) { - setState(() { - _isHovering = false; - }); - _hoverController.reverse(); - }, - child: Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(16), - child: InkWell( - onTap: () { - if (widget.onLogoutTap != null) { - HapticFeedback.mediumImpact(); - widget.onLogoutTap!(); - } - }, - borderRadius: BorderRadius.circular(16), - splashColor: Colors.red.withOpacity(0.1), - highlightColor: Colors.red.withOpacity(0.05), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - decoration: BoxDecoration( - color: _isHovering ? Colors.red.withOpacity(0.05) : Colors.transparent, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.red.withOpacity(_isHovering ? 0.3 : 0.1), - width: 1.5, - ), + final double iconSize = isSmallScreen ? 16.0 : 20.0; + final double fontSize = isSmallScreen ? 13.0 : 14.0; + final double buttonPadding = isSmallScreen ? 8.0 : 10.0; + + return Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(12), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () { + if (onLogoutTap != null) { + HapticFeedback.mediumImpact(); + onLogoutTap!(); + } + }, + borderRadius: BorderRadius.circular(12), + splashColor: Colors.red.withOpacity(0.1), + highlightColor: Colors.red.withOpacity(0.05), + child: Container( + padding: EdgeInsets.symmetric( + vertical: buttonPadding, horizontal: buttonPadding), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.red.withOpacity(0.2), + width: 1.0, ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.red.withOpacity(_isHovering ? 0.2 : 0.1), - borderRadius: BorderRadius.circular(12), - boxShadow: _isHovering ? [ - BoxShadow( - color: Colors.red.withOpacity(0.2), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ] : null, - ), - child: const Icon( - Icons.logout_rounded, - color: Colors.red, - size: 22, - ), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(isSmallScreen ? 6 : 8), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), ), - const SizedBox(width: 16), - Text( - 'Logout', - style: GoogleFonts.poppins( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.red, - ), + child: Icon( + Icons.logout_rounded, + color: Colors.red, + size: iconSize, ), - const Spacer(), - AnimatedContainer( - duration: const Duration(milliseconds: 300), - transform: Matrix4.translationValues( - _isHovering ? 5.0 : 0.0, 0.0, 0.0), - child: const Icon( - Icons.arrow_forward_ios_rounded, - color: Colors.red, - size: 16, - ), + ), + SizedBox(width: isSmallScreen ? 8 : 12), + Text( + 'Logout', + style: GoogleFonts.poppins( + fontSize: fontSize, + fontWeight: FontWeight.w600, + color: Colors.red, ), - ], - ), + ), + const Spacer(), + Icon( + Icons.arrow_forward_ios_rounded, + color: Colors.red, + size: isSmallScreen ? 12 : 14, + ), + ], ), ), ), @@ -156,30 +117,21 @@ class _NavFooterState extends State with SingleTickerProviderStateMix } Widget _buildAppVersion() { - return Align( - alignment: Alignment.center, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: EvacuationColors.borderColor.withOpacity(0.5), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: EvacuationColors.shadowColor.withOpacity(0.05), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Text( - 'App Version 1.0.0', - style: GoogleFonts.poppins( - color: EvacuationColors.subtitleColor, - fontSize: 12, - fontWeight: FontWeight.w500, - ), + return Container( + padding: EdgeInsets.symmetric( + horizontal: isSmallScreen ? 8 : 10, vertical: isSmallScreen ? 3 : 4), + decoration: BoxDecoration( + color: EvacuationColors.borderColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + 'App Version 1.0.0', + style: GoogleFonts.poppins( + color: EvacuationColors.subtitleColor, + fontSize: isSmallScreen ? 9 : 10, + fontWeight: FontWeight.w500, ), ), ); } -} \ No newline at end of file +} diff --git a/lib/features/disaster_alerts/widgets/SideNavigation/nav_header.dart b/lib/features/disaster_alerts/widgets/SideNavigation/nav_header.dart index 5f4cd6b..c84d432 100644 --- a/lib/features/disaster_alerts/widgets/SideNavigation/nav_header.dart +++ b/lib/features/disaster_alerts/widgets/SideNavigation/nav_header.dart @@ -2,14 +2,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../constants/colors.dart'; -import '../../common/animated_background.dart'; +import 'dart:math' as math; -class NavHeader extends StatefulWidget { +class NavHeader extends StatelessWidget { final String userName; final String? userEmail; final String? userAvatar; final VoidCallback? onProfileTap; final bool isEmergencyMode; + final bool isSmallScreen; const NavHeader({ Key? key, @@ -18,347 +19,213 @@ class NavHeader extends StatefulWidget { this.userAvatar, this.onProfileTap, this.isEmergencyMode = false, + this.isSmallScreen = false, }) : super(key: key); - @override - State createState() => _NavHeaderState(); -} - -class _NavHeaderState extends State with SingleTickerProviderStateMixin { - late AnimationController _animationController; - late Animation _pulseAnimation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 2000), - )..repeat(reverse: true); - - _pulseAnimation = Tween(begin: 1.0, end: 1.15).animate( - CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - ), - ); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { + // Calculate responsive sizes based on screen size + final size = MediaQuery.of(context).size; + final headerHeight = isSmallScreen + ? math.min(150.0, size.height * 0.25) + : math.min(180.0, size.height * 0.3); + final avatarSize = isSmallScreen ? 40.0 : 50.0; + final double horizontalPadding = isSmallScreen ? 12.0 : 16.0; + final double verticalPadding = isSmallScreen ? 8.0 : 12.0; + return Container( - height: 200, + height: headerHeight, width: double.infinity, - child: Stack( - children: [ - // Map-themed animated background - AnimatedBackground( - colors: widget.isEmergencyMode - ? [ - Color(0xFF7D0000), - Color(0xFFAA0000), - Color(0xFF7D0000), - ] - : [ - EvacuationColors.primaryColor, - EvacuationColors.primaryColor.withBlue(EvacuationColors.primaryColor.blue + 40), - EvacuationColors.accentColor, - ], - isEmergencyMode: widget.isEmergencyMode, - ), - - // Custom map elements overlay - Positioned.fill( - child: CustomPaint( - painter: NavigationLinesPainter( - progress: _animationController.value, - isEmergencyMode: widget.isEmergencyMode, - ), - ), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isEmergencyMode + ? [ + Color(0xFF7D0000), + Color(0xFFAA0000), + ] + : [ + EvacuationColors.primaryColor, + EvacuationColors.primaryColor.withBlue( + (EvacuationColors.primaryColor.blue + 40).clamp(0, 255)), + ], + ), + ), + child: SafeArea( + bottom: false, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, + vertical: verticalPadding, ), - - // Content - Positioned.fill( - child: Padding( - padding: EdgeInsets.only( - top: MediaQuery.of(context).padding.top + 20, - bottom: 20, - left: 20, - right: 20, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Profile section with GPS beacon effect - GestureDetector( - onTap: widget.onProfileTap, - onTapDown: (_) { - HapticFeedback.selectionClick(); - }, - child: Row( - children: [ - // Avatar with pulsing effect - AnimatedBuilder( - animation: _animationController, - builder: (context, child) { - return Stack( - alignment: Alignment.center, - children: [ - // Outer pulse - Transform.scale( - scale: _pulseAnimation.value, - child: Container( - width: 70, - height: 70, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: widget.isEmergencyMode - ? Colors.red.withOpacity(0.2) - : Colors.cyan.withOpacity(0.2), - ), - ), - ), - // Middle pulse - Transform.scale( - scale: _pulseAnimation.value * 0.9, - child: Container( - width: 60, - height: 60, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: widget.isEmergencyMode - ? Colors.red.withOpacity(0.3) - : Colors.cyan.withOpacity(0.3), - ), - ), - ), - // Avatar - Container( - padding: const EdgeInsets.all(3), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: widget.isEmergencyMode - ? Colors.red.withOpacity(0.8) - : Colors.white, - width: 2, - ), - boxShadow: [ - BoxShadow( - color: widget.isEmergencyMode - ? Colors.red.withOpacity(0.5) - : Colors.cyan.withOpacity(0.5), - blurRadius: 10, - spreadRadius: 2, - ), - ], - ), - child: CircleAvatar( - radius: 25, - backgroundColor: Colors.white, - backgroundImage: widget.userAvatar != null - ? NetworkImage(widget.userAvatar!) - : null, - child: widget.userAvatar == null - ? Text( - widget.userName[0].toUpperCase(), - style: GoogleFonts.poppins( - fontSize: 20, - fontWeight: FontWeight.bold, - color: EvacuationColors.primaryColor, - ), - ) - : null, - ), - ), - ], - ); - }, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Profile section with avatar and user info + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Avatar + GestureDetector( + onTap: onProfileTap, + onTapDown: (_) { + HapticFeedback.selectionClick(); + }, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + spreadRadius: 1, + ), + ], ), - - const SizedBox(width: 16), - - // User info - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.userName, - style: GoogleFonts.poppins( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (widget.userEmail != null) - Text( - widget.userEmail!, + child: CircleAvatar( + radius: avatarSize / 2 - 2, + backgroundColor: Colors.white, + backgroundImage: userAvatar != null + ? NetworkImage(userAvatar!) + : null, + child: userAvatar == null + ? Text( + userName.isNotEmpty + ? userName[0].toUpperCase() + : "U", style: GoogleFonts.poppins( - fontSize: 14, - color: Colors.white.withOpacity(0.8), + fontSize: isSmallScreen ? 16 : 18, + fontWeight: FontWeight.bold, + color: EvacuationColors.primaryColor, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - - // Location indicator - Container( - margin: const EdgeInsets.only(top: 4), - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.location_on, - color: widget.isEmergencyMode - ? Colors.red - : Colors.cyan, - size: 12, - ), - const SizedBox(width: 4), - Text( - 'Current Location', - style: GoogleFonts.poppins( - fontSize: 12, - color: Colors.white.withOpacity(0.9), - ), - ), - ], - ), - ), - ], - ), + ) + : null, ), - ], + ), ), - ), - - const Spacer(), - - // View Profile button - InkWell( - onTap: widget.onProfileTap, - borderRadius: BorderRadius.circular(20), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, + + SizedBox(width: horizontalPadding), + + // User info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + userName, + style: GoogleFonts.poppins( + fontSize: isSmallScreen ? 16 : 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (userEmail != null) + Text( + userEmail!, + style: GoogleFonts.poppins( + fontSize: isSmallScreen ? 11 : 12, + color: Colors.white.withOpacity(0.8), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), + ), + ], + ), + ), + + // Spacer + Spacer(flex: 1), + + // Status card + Container( + constraints: BoxConstraints( + maxHeight: isSmallScreen ? 60 : 70, + ), + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding / 1.5, + vertical: verticalPadding / 1.5), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.white.withOpacity(0.2), + width: 1, + ), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(isSmallScreen ? 6 : 8), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: Colors.white.withOpacity(0.3), - width: 1, - ), + color: isEmergencyMode + ? Colors.red.withOpacity(0.3) + : Colors.white.withOpacity(0.3), + shape: BoxShape.circle, + ), + child: Icon( + isEmergencyMode + ? Icons.warning_rounded + : Icons.shield_rounded, + color: Colors.white, + size: isSmallScreen ? 14 : 16, ), - child: Row( + ), + SizedBox(width: horizontalPadding / 2), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( - 'View Profile', + isEmergencyMode + ? 'Emergency Mode Active' + : 'Status: Safe', style: GoogleFonts.poppins( + fontSize: isSmallScreen ? 12 : 13, + fontWeight: FontWeight.w600, color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w500, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - const SizedBox(width: 4), - const Icon( - Icons.arrow_forward_ios_rounded, - size: 12, - color: Colors.white, + Text( + isEmergencyMode + ? 'Emergency services notified' + : 'No active alerts in your area', + style: GoogleFonts.poppins( + fontSize: isSmallScreen ? 10 : 11, + color: Colors.white.withOpacity(0.8), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), ), - ), - ], - ), - ), - ), - - // Bottom wave decoration - Positioned( - bottom: 0, - left: 0, - right: 0, - child: SizedBox( - height: 20, - child: ClipPath( - clipper: WaveClipper(), - child: Container( - color: Colors.white, + ], ), ), - ), + + // Small bottom space + SizedBox(height: verticalPadding / 2), + ], ), - ], + ), ), ); } } - -// Wave clipper for the bottom decoration -class WaveClipper extends CustomClipper { - @override - Path getClip(Size size) { - final path = Path(); - path.lineTo(0, 0); - - // First wave - final firstControlPoint = Offset(size.width / 4, size.height); - final firstEndPoint = Offset(size.width / 2.25, size.height / 2); - path.quadraticBezierTo( - firstControlPoint.dx, - firstControlPoint.dy, - firstEndPoint.dx, - firstEndPoint.dy, - ); - - // Second wave - final secondControlPoint = Offset(size.width / 1.8, 0); - final secondEndPoint = Offset(size.width / 1.25, size.height / 2); - path.quadraticBezierTo( - secondControlPoint.dx, - secondControlPoint.dy, - secondEndPoint.dx, - secondEndPoint.dy, - ); - - // Third wave - final thirdControlPoint = Offset(size.width / 1.1, size.height); - final thirdEndPoint = Offset(size.width, 0); - path.quadraticBezierTo( - thirdControlPoint.dx, - thirdControlPoint.dy, - thirdEndPoint.dx, - thirdEndPoint.dy, - ); - - path.lineTo(size.width, size.height); - path.lineTo(0, size.height); - path.close(); - - return path; - } - - @override - bool shouldReclip(CustomClipper oldClipper) => false; -} \ No newline at end of file diff --git a/lib/features/disaster_alerts/widgets/SideNavigation/nav_item.dart b/lib/features/disaster_alerts/widgets/SideNavigation/nav_item.dart index 43f905b..372fab5 100644 --- a/lib/features/disaster_alerts/widgets/SideNavigation/nav_item.dart +++ b/lib/features/disaster_alerts/widgets/SideNavigation/nav_item.dart @@ -4,11 +4,12 @@ import 'package:google_fonts/google_fonts.dart'; import '../../models/navigation_item.dart'; import '../../constants/colors.dart'; -class NavItem extends StatefulWidget { +class NavItem extends StatelessWidget { final NavigationItem item; final bool isSelected; final int index; final VoidCallback onTap; + final bool isSmallScreen; const NavItem({ Key? key, @@ -16,79 +17,11 @@ class NavItem extends StatefulWidget { required this.isSelected, required this.index, required this.onTap, + this.isSmallScreen = false, }) : super(key: key); - @override - State createState() => _NavItemState(); -} - -class _NavItemState extends State with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _scaleAnimation; - late Animation _iconScaleAnimation; - late Animation _alertPulseAnimation; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 300), - ); - - _scaleAnimation = Tween(begin: 1.0, end: 0.97).animate( - CurvedAnimation( - parent: _controller, - curve: Curves.easeOutCubic, - ), - ); - - _iconScaleAnimation = Tween(begin: 1.0, end: 1.1).animate( - CurvedAnimation( - parent: _controller, - curve: Curves.elasticOut, - ), - ); - - _alertPulseAnimation = TweenSequence([ - TweenSequenceItem( - tween: Tween(begin: 1.0, end: 1.2), - weight: 1.0, - ), - TweenSequenceItem( - tween: Tween(begin: 1.2, end: 1.0), - weight: 1.0, - ), - ]).animate(CurvedAnimation( - parent: _controller, - curve: Curves.easeInOut, - )); - - if (widget.item.hasAlert) { - _controller.repeat(reverse: true); - } - } - - @override - void didUpdateWidget(NavItem oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.isSelected != oldWidget.isSelected) { - if (widget.isSelected) { - _controller.forward(from: 0).then((_) => _controller.reverse()); - } - } - - if (widget.item.hasAlert != oldWidget.item.hasAlert) { - if (widget.item.hasAlert) { - _controller.repeat(reverse: true); - } else { - _controller.stop(); - } - } - } - Color _getAlertColor() { - switch (widget.item.alertLevel) { + switch (item.alertLevel) { case AlertLevel.high: return Colors.red; case AlertLevel.medium: @@ -100,261 +33,140 @@ class _NavItemState extends State with SingleTickerProviderStateMixin { } } - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: 1.0), - duration: Duration(milliseconds: 400 + (widget.index * 100)), - curve: Curves.easeOutQuint, - builder: (context, value, child) { - return Transform.translate( - offset: Offset(30 * (1 - value), 0), - child: Opacity( - opacity: value, - child: child, - ), - ); - }, - child: Padding( - padding: const EdgeInsets.only(bottom: 8), - child: GestureDetector( + final double fontSize = isSmallScreen ? 13.0 : 14.0; + final double iconSize = isSmallScreen ? 20.0 : 22.0; + final double verticalPadding = isSmallScreen ? 8.0 : 10.0; + final double horizontalPadding = isSmallScreen ? 10.0 : 12.0; + final double iconContainerPadding = isSmallScreen ? 6.0 : 8.0; + + final alertColor = _getAlertColor(); + final hasAlert = item.hasAlert; + + return Padding( + padding: EdgeInsets.only(bottom: isSmallScreen ? 4 : 6), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(12), + clipBehavior: Clip.antiAlias, + child: InkWell( onTap: () { HapticFeedback.selectionClick(); - widget.onTap(); + onTap(); }, - onTapDown: (_) => _controller.forward(), - onTapUp: (_) => _controller.reverse(), - onTapCancel: () => _controller.reverse(), - child: AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Transform.scale( - scale: _scaleAnimation.value, - child: child, - ); - }, - child: Stack( + splashColor: EvacuationColors.primaryColor.withOpacity(0.1), + highlightColor: EvacuationColors.primaryColor.withOpacity(0.05), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, vertical: verticalPadding), + decoration: BoxDecoration( + color: isSelected + ? (hasAlert + ? alertColor.withOpacity(0.1) + : EvacuationColors.primaryColor.withOpacity(0.1)) + : Colors.transparent, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? (hasAlert ? alertColor : EvacuationColors.primaryColor) + : Colors.transparent, + width: 1.5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.max, children: [ - _buildNavItemContent(), - if (widget.item.hasAlert) - Positioned( - right: 0, - top: 0, - child: AnimatedBuilder( - animation: _alertPulseAnimation, - builder: (context, child) { - return Transform.scale( - scale: _alertPulseAnimation.value, - child: Container( - width: 12, - height: 12, - decoration: BoxDecoration( - color: _getAlertColor(), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: _getAlertColor().withOpacity(0.5), - blurRadius: 6, - spreadRadius: 2, - ), - ], - ), - ), - ); - }, - ), + // Item icon with container + Container( + padding: EdgeInsets.all(iconContainerPadding), + decoration: BoxDecoration( + color: isSelected + ? (hasAlert + ? alertColor.withOpacity(0.2) + : EvacuationColors.primaryColor.withOpacity(0.2)) + : Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), ), - ], - ), - ), - ), - ), - ); - } - - Widget _buildNavItemContent() { - final alertColor = _getAlertColor(); - final hasAlert = widget.item.hasAlert; + child: Icon( + item.icon, + color: isSelected + ? (hasAlert + ? alertColor + : EvacuationColors.primaryColor) + : Colors.grey.shade700, + size: iconSize, + ), + ), - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - decoration: BoxDecoration( - color: widget.isSelected - ? (hasAlert ? alertColor.withOpacity(0.1) : EvacuationColors.primaryColor.withOpacity(0.1)) - : Colors.grey.withOpacity(0.07), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: widget.isSelected - ? (hasAlert ? alertColor : EvacuationColors.primaryColor) - : Colors.transparent, - width: 1.5, - ), - boxShadow: widget.isSelected - ? [ - BoxShadow( - color: (hasAlert ? alertColor : EvacuationColors.primaryColor).withOpacity(0.2), - blurRadius: 8, - offset: const Offset(0, 2), + SizedBox(width: isSmallScreen ? 8 : 10), + + // Item title + Expanded( + child: Text( + item.title, + style: GoogleFonts.poppins( + fontSize: fontSize, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w500, + color: isSelected + ? (hasAlert + ? alertColor + : EvacuationColors.primaryColor) + : Colors.grey.shade800, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), - ] - : null, - ), - child: Row( - children: [ - // Icon container with map-themed animation - AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Stack( - alignment: Alignment.center, - children: [ - // Animated background for selected items - if (widget.isSelected) - Transform.scale( - scale: _iconScaleAnimation.value * 0.9, - child: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: (hasAlert ? alertColor : EvacuationColors.primaryColor).withOpacity(0.2), - ), + + // Badge or notification count if available + if (item.badge != null) + Container( + padding: EdgeInsets.symmetric( + horizontal: isSmallScreen ? 5 : 6, + vertical: isSmallScreen ? 1 : 2, + ), + margin: EdgeInsets.only(left: isSmallScreen ? 4 : 6), + decoration: BoxDecoration( + color: isSelected + ? EvacuationColors.primaryColor + : Colors.grey.shade600, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + item.badge!, + style: GoogleFonts.poppins( + fontSize: isSmallScreen ? 10 : 11, + fontWeight: FontWeight.w500, + color: Colors.white, ), ), - - // Icon container + ), + + // Alert indicator + if (hasAlert && item.badge == null) Container( - padding: const EdgeInsets.all(10), + width: isSmallScreen ? 6 : 8, + height: isSmallScreen ? 6 : 8, + margin: EdgeInsets.only(left: isSmallScreen ? 4 : 6), decoration: BoxDecoration( - color: widget.isSelected - ? (hasAlert ? alertColor : EvacuationColors.primaryColor) - : Colors.white, - borderRadius: BorderRadius.circular(12), + color: alertColor, + shape: BoxShape.circle, boxShadow: [ BoxShadow( - color: widget.isSelected - ? (hasAlert ? alertColor : EvacuationColors.primaryColor).withOpacity(0.3) - : Colors.black.withOpacity(0.05), - blurRadius: 8, - offset: const Offset(0, 2), + color: alertColor.withOpacity(0.5), + blurRadius: 4, + spreadRadius: 1, ), ], ), - child: Icon( - widget.item.icon, - color: widget.isSelected - ? Colors.white - : (hasAlert ? alertColor : widget.item.iconColor ?? EvacuationColors.primaryColor), - size: 24, - ), - ), - - // Ripple effect for selected items - if (widget.isSelected) - ...List.generate(2, (index) { - final delay = index * 0.4; - final progress = (_controller.value - delay) % 1.0; - - // Only show if within the visible range - if (progress < 0) return const SizedBox(); - - return Transform.scale( - scale: 0.5 + (progress * 0.8), - child: Opacity( - opacity: (1.0 - progress) * 0.4, - child: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: hasAlert ? alertColor : EvacuationColors.primaryColor, - width: 2, - ), - ), - ), - ), - ); - }), - ], - ); - }, - ), - - const SizedBox(width: 16), - - // Title with animated underline for selected items - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.item.title, - style: GoogleFonts.poppins( - fontSize: 16, - fontWeight: widget.isSelected - ? FontWeight.w600 - : FontWeight.w500, - color: widget.isSelected - ? (hasAlert ? alertColor : EvacuationColors.primaryColor) - : EvacuationColors.textColor, - ), - ), - - // Animated underline for selected items - if (widget.isSelected) - AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Container( - margin: const EdgeInsets.only(top: 4), - height: 2, - width: 20 + (30 * _controller.value), - decoration: BoxDecoration( - color: hasAlert ? alertColor : EvacuationColors.primaryColor, - borderRadius: BorderRadius.circular(1), - ), - ); - }, ), ], ), ), - - // Badge or distance indicator - if (widget.item.badge != null) - AnimatedContainer( - duration: const Duration(milliseconds: 300), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: widget.item.badgeColor ?? EvacuationColors.primaryColor, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: (widget.item.badgeColor ?? EvacuationColors.primaryColor) - .withOpacity(0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Text( - widget.item.badge!, - style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ), - ], + ), ), ); } -} \ No newline at end of file +} diff --git a/lib/features/disaster_alerts/widgets/SideNavigation/side_navigation.dart b/lib/features/disaster_alerts/widgets/SideNavigation/side_navigation.dart index 22cfa8b..c8175b8 100644 --- a/lib/features/disaster_alerts/widgets/SideNavigation/side_navigation.dart +++ b/lib/features/disaster_alerts/widgets/SideNavigation/side_navigation.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:lottie/lottie.dart'; import 'nav_header.dart'; import 'nav_item.dart'; import 'nav_footer.dart'; -import '../../common/animated_background.dart'; import '../../models/navigation_item.dart'; import '../../constants/colors.dart'; +import 'dart:math' as math; class SideNavigation extends StatefulWidget { final String userName; @@ -32,9 +31,7 @@ class SideNavigation extends StatefulWidget { State createState() => _SideNavigationState(); } -class _SideNavigationState extends State - with SingleTickerProviderStateMixin { - late AnimationController _animationController; +class _SideNavigationState extends State { late int _selectedIndex; final ScrollController _scrollController = ScrollController(); final List _navItems = []; @@ -44,13 +41,6 @@ class _SideNavigationState extends State super.initState(); _selectedIndex = widget.initialSelectedIndex; - // Animation controller for entrance and exit animations - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 400), - ); - _animationController.forward(); - // Initialize navigation items _initNavigationItems(); } @@ -93,7 +83,6 @@ class _SideNavigationState extends State @override void dispose() { - _animationController.dispose(); _scrollController.dispose(); super.dispose(); } @@ -120,99 +109,104 @@ class _SideNavigationState extends State @override Widget build(BuildContext context) { - return Drawer( - backgroundColor: EvacuationColors.backgroundColor, - elevation: 0, - child: AnimatedBuilder( - animation: _animationController, - builder: (context, child) { - return SlideTransition( - position: Tween( - begin: const Offset(-0.2, 0), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeOutCubic, - )), - child: FadeTransition( - opacity: _animationController, - child: child, - ), - ); - }, - child: Column( - children: [ - // Header with user profile - NavHeader( - userName: widget.userName, - userEmail: widget.userEmail, - userAvatar: widget.userAvatar, - onProfileTap: () { - Navigator.pop(context); - Navigator.pushNamed(context, '/profile'); - }, - ), + final Size screenSize = MediaQuery.of(context).size; + final bool isSmallScreen = + screenSize.width < 360 || screenSize.height < 600; + final double drawerWidth = math.min( + isSmallScreen ? screenSize.width * 0.85 : 300, + screenSize.width - 40 // Maximum width constraint + ); - // Navigation items - Expanded( - child: Theme( - data: Theme.of(context).copyWith( - scrollbarTheme: ScrollbarThemeData( - thumbColor: MaterialStateProperty.all( - EvacuationColors.primaryColor.withOpacity(0.5), - ), - radius: const Radius.circular(10), - thickness: MaterialStateProperty.all(5), - ), + return SizedBox( + width: drawerWidth, + child: Drawer( + width: drawerWidth, + backgroundColor: EvacuationColors.backgroundColor, + elevation: 0, + child: SafeArea( + child: LayoutBuilder(builder: (context, constraints) { + return Column( + children: [ + // Header with user profile + NavHeader( + userName: widget.userName, + userEmail: widget.userEmail, + userAvatar: widget.userAvatar, + onProfileTap: () { + Navigator.pop(context); + Navigator.pushNamed(context, '/profile'); + }, + isSmallScreen: isSmallScreen, ), - child: Scrollbar( - controller: _scrollController, - thumbVisibility: true, - child: SingleChildScrollView( - controller: _scrollController, - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.symmetric( - vertical: 16, - horizontal: 12, + + // Navigation items - wrap in Flexible to avoid overflow + Flexible( + child: Theme( + data: Theme.of(context).copyWith( + scrollbarTheme: ScrollbarThemeData( + thumbColor: MaterialStateProperty.all( + EvacuationColors.primaryColor.withOpacity(0.5), + ), + radius: const Radius.circular(10), + thickness: MaterialStateProperty.all(5), + ), ), - child: Column( - children: List.generate( - _navItems.length, - (index) => NavItem( - item: _navItems[index], - isSelected: _selectedIndex == index, - index: index, - onTap: () => _handleItemTap(index), + child: Scrollbar( + controller: _scrollController, + thumbVisibility: true, + child: ListView( + controller: _scrollController, + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.symmetric( + vertical: isSmallScreen ? 8 : 16, + horizontal: isSmallScreen ? 8 : 12, + ), + children: List.generate( + _navItems.length, + (index) => NavItem( + item: _navItems[index], + isSelected: _selectedIndex == index, + index: index, + onTap: () => _handleItemTap(index), + isSmallScreen: isSmallScreen, + ), ), ), ), ), ), - ), - ), - // Footer with logout button - NavFooter( - onLogoutTap: () { - HapticFeedback.mediumImpact(); - // Show confirmation dialog - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - isScrollControlled: true, - builder: (context) => _buildLogoutConfirmation(context), - ); - }, - ), - ], + // Footer with logout button + NavFooter( + onLogoutTap: () { + HapticFeedback.mediumImpact(); + // Show confirmation dialog + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) => + _buildLogoutConfirmation(context, isSmallScreen), + ); + }, + isSmallScreen: isSmallScreen, + ), + ], + ); + }), ), ), ); } - Widget _buildLogoutConfirmation(BuildContext context) { + Widget _buildLogoutConfirmation(BuildContext context, bool isSmallScreen) { + final double modalHeight = math.min( + MediaQuery.of(context).size.height * (isSmallScreen ? 0.2 : 0.25), + isSmallScreen ? 180.0 : 220.0 // Maximum height constraint + ); + return Container( - height: MediaQuery.of(context).size.height * 0.25, + height: modalHeight, decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), @@ -236,71 +230,73 @@ class _SideNavigationState extends State borderRadius: BorderRadius.circular(10), ), ), - const SizedBox(height: 20), + const SizedBox(height: 16), Text( 'Logout', style: GoogleFonts.poppins( - fontSize: 20, + fontSize: isSmallScreen ? 18 : 20, fontWeight: FontWeight.bold, color: Colors.black, ), ), - const SizedBox(height: 12), + SizedBox(height: isSmallScreen ? 8 : 12), Text( 'Are you sure you want to logout?', style: GoogleFonts.poppins( - fontSize: 16, + fontSize: isSmallScreen ? 14 : 16, color: Colors.black87, ), ), - const SizedBox(height: 24), + Spacer(), Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), + padding: EdgeInsets.only(bottom: isSmallScreen ? 16 : 24), child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: OutlinedButton( - onPressed: () => Navigator.pop(context), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - side: BorderSide(color: EvacuationColors.primaryColor), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + OutlinedButton( + onPressed: () => Navigator.pop(context), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric( + horizontal: isSmallScreen ? 16 : 24, + vertical: isSmallScreen ? 8 : 12, ), - child: Text( - 'Cancel', - style: GoogleFonts.poppins( - fontSize: 16, - fontWeight: FontWeight.w600, - color: EvacuationColors.primaryColor, - ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + side: BorderSide(color: EvacuationColors.primaryColor), + ), + child: Text( + 'Cancel', + style: GoogleFonts.poppins( + fontSize: isSmallScreen ? 14 : 16, + fontWeight: FontWeight.w500, + color: EvacuationColors.primaryColor, ), ), ), - const SizedBox(width: 16), - Expanded( - child: ElevatedButton( - onPressed: () { - // Handle logout - Navigator.pop(context); - Navigator.pushReplacementNamed(context, '/login'); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - padding: const EdgeInsets.symmetric(vertical: 16), - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + SizedBox(width: isSmallScreen ? 12 : 20), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + // Handle logout action + Navigator.pushReplacementNamed(context, '/login'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + padding: EdgeInsets.symmetric( + horizontal: isSmallScreen ? 16 : 24, + vertical: isSmallScreen ? 8 : 12, ), - child: Text( - 'Logout', - style: GoogleFonts.poppins( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Text( + 'Logout', + style: GoogleFonts.poppins( + fontSize: isSmallScreen ? 14 : 16, + fontWeight: FontWeight.w500, + color: Colors.white, ), ), ), diff --git a/lib/features/disaster_alerts/widgets/evacuation_map.dart b/lib/features/disaster_alerts/widgets/evacuation_map.dart index 30fd6af..cf2a9d6 100644 --- a/lib/features/disaster_alerts/widgets/evacuation_map.dart +++ b/lib/features/disaster_alerts/widgets/evacuation_map.dart @@ -11,6 +11,8 @@ import 'evacuation_map/map_controls.dart'; import 'evacuation_map/route_info_panel.dart'; import 'evacuation_map/route_animation.dart'; import 'evacuation_map/route_calculation.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:math' as math; class EvacuationMap extends StatefulWidget { final LatLng currentPosition; @@ -84,20 +86,16 @@ class _EvacuationMapState extends State if (widget.polylines != oldWidget.polylines) { _updateRouteDetails(); if (widget.polylines.isNotEmpty) { - _routeAnimator.startAnimation( - widget.polylines, - (polylines) { + _routeAnimator.startAnimation(widget.polylines, (polylines) { setState(() { // This updates the UI when animation progresses _isAnimatingRoute = true; }); - }, - () { + }, () { setState(() { _isAnimatingRoute = false; }); - } - ); + }); } } } @@ -174,24 +172,42 @@ class _EvacuationMapState extends State // Replace your existing _buildRouteInfoPanel with this floating version Widget _buildFloatingRouteInfoPanel() { + // Get screen width to calculate adaptive spacing + final screenWidth = MediaQuery.of(context).size.width; + final isSmallScreen = screenWidth < 360; + + // Calculate adaptive sizes + final double iconSize = isSmallScreen ? 18 : 20; + final double fontSize = isSmallScreen ? 12 : 14; + final double buttonFontSize = isSmallScreen ? 12 : 14; + final double padding = isSmallScreen ? 8 : 12; + final double spacing = isSmallScreen ? 6 : 12; + return Container( - padding: const EdgeInsets.all(12), + padding: EdgeInsets.symmetric(horizontal: padding, vertical: padding / 2), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.9), // Semi-transparent background + color: Colors.white + .withOpacity(0.95), // More opaque for better readability borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), + color: Colors.black.withOpacity(0.15), + blurRadius: 10, + offset: const Offset(0, 3), + spreadRadius: 1, ), ], ), - child: Row( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Top row with route info + Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ // Route icon Container( - padding: const EdgeInsets.all(8), + padding: EdgeInsets.all(padding * 0.75), decoration: BoxDecoration( color: EvacuationColors.primaryColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), @@ -199,31 +215,31 @@ class _EvacuationMapState extends State child: Icon( Icons.directions, color: EvacuationColors.primaryColor, - size: 20, + size: iconSize, ), ), // Route details Expanded( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), + padding: EdgeInsets.symmetric(horizontal: spacing), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, + mainAxisSize: MainAxisSize.min, children: [ Text( 'Route to Destination', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, + style: GoogleFonts.inter( + fontSize: fontSize, + fontWeight: FontWeight.w600, color: EvacuationColors.textColor, ), ), - const SizedBox(height: 4), + SizedBox(height: spacing / 3), Text( '$_routeDistance km • $_routeDuration min', - style: TextStyle( - fontSize: 12, + style: GoogleFonts.inter( + fontSize: fontSize - 2, color: EvacuationColors.subtitleColor, ), ), @@ -231,23 +247,77 @@ class _EvacuationMapState extends State ), ), ), + ], + ), - // Navigation button - ElevatedButton.icon( - onPressed: () { - // Start navigation logic + // Spacing between rows + SizedBox(height: spacing * 0.75), + + // Button row + Container( + width: double.infinity, + child: GestureDetector( + onTap: () { + _showAIRoutingDialog(); + // Start navigation logic after AI "processing" + Future.delayed(const Duration(milliseconds: 2500), () { if (widget.onNavigationStarted != null) { widget.onNavigationStarted!(true); // Request map expansion } - }, - icon: const Icon(Icons.navigation, size: 16), - label: const Text('Navigate'), - style: ElevatedButton.styleFrom( - backgroundColor: EvacuationColors.primaryColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), - shape: RoundedRectangleBorder( + }); + }, + child: Container( + padding: EdgeInsets.symmetric( + vertical: padding * 0.75, horizontal: padding), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + EvacuationColors.primaryColor, + EvacuationColors.accentColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: EvacuationColors.primaryColor.withOpacity(0.2), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Animated navigation icon + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(seconds: 2), + curve: Curves.easeInOut, + builder: (context, value, child) { + return Transform.rotate( + angle: value * 0.1 * math.sin(value * 10), + child: Icon( + Icons.navigation_rounded, + color: Colors.white, + size: iconSize, + ), + ); + }, + ), + SizedBox(width: spacing / 2), + // Button text + Text( + 'AI-Optimized Route', + style: GoogleFonts.inter( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: buttonFontSize, + ), + ), + ], + ), ), ), ), @@ -275,7 +345,7 @@ class _EvacuationMapState extends State onDarkModeToggled: () { setState(() { _isDarkMode = !_isDarkMode; - MapStyleUtils.setMapStyle(_mapController,_isDarkMode); + MapStyleUtils.setMapStyle(_mapController, _isDarkMode); }); }, onMyLocationPressed: () { @@ -424,18 +494,180 @@ class _EvacuationMapState extends State _isAnimatingRoute = true; }); - _routeAnimator.startAnimation( - widget.polylines, - (polylines) { + _routeAnimator.startAnimation(widget.polylines, (polylines) { setState(() { // This updates the UI when animation progresses }); - }, - () { + }, () { setState(() { _isAnimatingRoute = false; - }); - } + }); + }); + } + + // Add this method to show the AI routing dialog + void _showAIRoutingDialog() { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + spreadRadius: 1, + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // AI processing animation + SizedBox( + height: 80, + width: 80, + child: Stack( + alignment: Alignment.center, + children: [ + // Outer rotating circle + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 4.0), + duration: const Duration(seconds: 2), + builder: (context, value, child) { + return Transform.rotate( + angle: value * math.pi, + child: Container( + height: 80, + width: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: EvacuationColors.primaryColor + .withOpacity(0.3), + width: 3, + strokeAlign: BorderSide.strokeAlignOutside, + ), + gradient: SweepGradient( + colors: [ + EvacuationColors.primaryColor + .withOpacity(0.0), + EvacuationColors.primaryColor, + ], + stops: const [0.7, 1.0], + ), + ), + ), + ); + }, + ), + // Inner pulsing circle + TweenAnimationBuilder( + tween: Tween(begin: 0.8, end: 1.0), + duration: const Duration(milliseconds: 800), + curve: Curves.easeInOut, + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: Container( + height: 50, + width: 50, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: EvacuationColors.primaryColor + .withOpacity(0.2), + ), + child: Icon( + Icons.route_rounded, + color: EvacuationColors.primaryColor, + size: 24, + ), + ), + ); + }, + ), + ], + ), + ), + const SizedBox(height: 20), + // Processing text + Text( + 'AI Optimizing Route', + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w700, + color: EvacuationColors.textColor, + ), + ), + const SizedBox(height: 12), + // Processing message + Text( + 'Analyzing traffic, weather conditions, and emergency factors to find the safest and fastest evacuation route.', + textAlign: TextAlign.center, + style: GoogleFonts.inter( + fontSize: 14, + color: EvacuationColors.subtitleColor, + ), + ), + const SizedBox(height: 16), + // Processing indicators + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildProcessingIndicator('Traffic', Colors.orange), + const SizedBox(width: 12), + _buildProcessingIndicator('Weather', Colors.blue), + const SizedBox(width: 12), + _buildProcessingIndicator('Safety', Colors.green), + ], + ), + ], + ), + ), + ), + ); + + // Close dialog after 2 seconds + Future.delayed(const Duration(milliseconds: 2300), () { + Navigator.of(context).pop(); + }); + } + + // Helper method to build processing indicators + Widget _buildProcessingIndicator(String label, Color color) { + return Column( + children: [ + SizedBox( + width: 50, + height: 4, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 2000), + builder: (context, value, child) { + return LinearProgressIndicator( + value: value, + backgroundColor: color.withOpacity(0.2), + valueColor: AlwaysStoppedAnimation(color), + borderRadius: BorderRadius.circular(2), + ); + }, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: GoogleFonts.inter( + fontSize: 10, + color: EvacuationColors.subtitleColor, + ), + ), + ], ); } } diff --git a/lib/features/disaster_alerts/widgets/expanded_map_preview.dart b/lib/features/disaster_alerts/widgets/expanded_map_preview.dart index 0efb0f3..81d0102 100644 --- a/lib/features/disaster_alerts/widgets/expanded_map_preview.dart +++ b/lib/features/disaster_alerts/widgets/expanded_map_preview.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'dart:math' as math; import '../models/evacuation_place.dart'; import '../constants/colors.dart'; import 'evacuation_map.dart'; -class ExpandedMapPreview extends StatelessWidget { +class ExpandedMapPreview extends StatefulWidget { final EvacuationPlace place; final LatLng currentPosition; final Set polylines; @@ -19,6 +20,58 @@ class ExpandedMapPreview extends StatelessWidget { required this.onMapCreated, }); + @override + State createState() => _ExpandedMapPreviewState(); +} + +class _ExpandedMapPreviewState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _pulseAnimation; + bool _showAIOverlay = true; + + // AI analysis factors + final List _aiFactors = [ + 'Traffic density', + 'Road conditions', + 'Weather impact', + 'Emergency services', + 'Evacuation flows', + 'Hazard proximity' + ]; + + // AI routes being compared (for animation purposes) + final List _routeScores = [78, 85, 64, 92]; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + )..repeat(reverse: true); + + _pulseAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ); + + // Auto-hide AI overlay after 5 seconds + Future.delayed(const Duration(seconds: 5), () { + if (mounted) { + setState(() { + _showAIOverlay = false; + }); + } + }); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Padding( @@ -26,39 +79,425 @@ class ExpandedMapPreview extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Enhanced header with AI badge + Row( + children: [ + Icon( + Icons.route_rounded, + color: EvacuationColors.primaryColor, + size: 20, + ), + const SizedBox(width: 8), Text( - 'Route Preview', + 'AI Route Analysis', style: GoogleFonts.inter( fontSize: 18, fontWeight: FontWeight.w600, color: EvacuationColors.textColor, ), ), - const SizedBox(height: 16), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + EvacuationColors.primaryColor, + EvacuationColors.accentColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.auto_awesome, + color: Colors.white, + size: 12, + ), + const SizedBox(width: 4), + Text( + 'AI Powered', + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ), + ), + const Spacer(), + // Toggle AI overlay button + IconButton( + icon: Icon( + _showAIOverlay ? Icons.visibility : Icons.visibility_off, + color: EvacuationColors.primaryColor, + size: 20, + ), + onPressed: () { + setState(() { + _showAIOverlay = !_showAIOverlay; + }); + }, + ), + ], + ), + + // Map container with AI overlay + const SizedBox(height: 12), + Stack( + children: [ + // Map container with animated shadow + AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + return Container( + height: 250, // Increased height + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: EvacuationColors.primaryColor + .withOpacity(0.1 + 0.1 * _pulseAnimation.value), + blurRadius: 16 + 8 * _pulseAnimation.value, + offset: const Offset(0, 4), + spreadRadius: 1 + _pulseAnimation.value, + ), + ], + ), + child: child, + ); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: EvacuationMap( + currentPosition: widget.currentPosition, + places: [widget.place], + polylines: widget.polylines, + onMapCreated: widget.onMapCreated, + ), + ), + ), + + // AI overlay elements (only shown when _showAIOverlay is true) + if (_showAIOverlay) + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: Stack( + children: [ + // Route analysis grid + Positioned( + right: 0, + top: 0, + bottom: 0, + width: 120, + child: Container( + color: Colors.black.withOpacity(0.7), + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'ANALYSIS', + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.w700, + color: Colors.white.withOpacity(0.7), + letterSpacing: 1.2, + ), + ), + const SizedBox(height: 8), + Expanded( + child: _buildFactorsList(), + ), + ], + ), + ), + ), + + // Route comparison overlay + Positioned( + left: 0, + bottom: 0, + right: 120, // Avoid overlapping with analysis panel + child: Container( + height: 60, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withOpacity(0.8), + Colors.transparent, + ], + ), + ), + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.looks_one_rounded, + color: Colors.white, + size: 14, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Text( + 'Optimal Route', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + const SizedBox(width: 4), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.green, + borderRadius: + BorderRadius.circular(4), + ), + child: Text( + '92%', + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + 'AI selected for safest evacuation', + style: GoogleFonts.inter( + fontSize: 10, + color: Colors.white.withOpacity(0.7), + ), + ), + ], + ), + ), + ], + ), + ), + ), + + // Animated scanning effect + AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Positioned( + left: 0, + right: 0, + top: _animationController.value * 250, + height: 2, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.transparent, + EvacuationColors.primaryColor + .withOpacity(0.8), + Colors.transparent, + ], + stops: const [0.0, 0.5, 1.0], + ), + ), + ), + ); + }, + ), + ], + ), + ), + ), + ], + ), + + // Route info section + const SizedBox(height: 12), Container( - height: 200, + padding: const EdgeInsets.all(12), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), + color: Colors.white, + borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: EvacuationColors.shadowColor, - blurRadius: 16, - offset: const Offset(0, 4), + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, 2), ), ], ), - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: EvacuationMap( - currentPosition: currentPosition, - places: [place], - polylines: polylines, - onMapCreated: onMapCreated, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'AI-Optimized for Your Safety', + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w700, + color: EvacuationColors.textColor, + ), + ), + const SizedBox(height: 8), + Text( + 'Our AI has analyzed multiple route options and selected the safest path considering traffic conditions, road closures, and hazard proximity.', + style: GoogleFonts.inter( + fontSize: 12, + color: EvacuationColors.subtitleColor, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + _buildInfoItem(Icons.speed_rounded, 'Fastest', 'route'), + const SizedBox(width: 8), + _buildInfoItem(Icons.shield_rounded, 'Safest', 'option'), + const SizedBox(width: 8), + _buildInfoItem(Icons.route_rounded, 'Optimal', 'path'), + ], + ), + ], ), ), ], ), ); } -} \ No newline at end of file + + Widget _buildFactorsList() { + return ListView.builder( + itemCount: _aiFactors.length, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _getAnalysisColor(index), + ), + ), + const SizedBox(width: 6), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _aiFactors[index], + style: GoogleFonts.inter( + fontSize: 9, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + const SizedBox(height: 2), + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 1500 + (index * 300)), + curve: Curves.easeOut, + builder: (context, value, child) { + return Container( + height: 3, + width: 70 * value, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + gradient: LinearGradient( + colors: [ + _getAnalysisColor(index), + _getAnalysisColor(index).withOpacity(0.5), + ], + ), + ), + ); + }, + ), + ], + ), + ), + ], + ), + ); + }, + ); + } + + Color _getAnalysisColor(int index) { + final colors = [ + Colors.green, + Colors.blue, + Colors.orange, + Colors.purple, + Colors.teal, + Colors.amber, + ]; + return colors[index % colors.length]; + } + + Widget _buildInfoItem(IconData icon, String title, String subtitle) { + return Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + decoration: BoxDecoration( + color: EvacuationColors.primaryColor.withOpacity(0.05), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + color: EvacuationColors.primaryColor, + size: 16, + ), + const SizedBox(height: 4), + Text( + title, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: EvacuationColors.textColor, + ), + ), + Text( + subtitle, + style: GoogleFonts.inter( + fontSize: 10, + color: EvacuationColors.subtitleColor, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/disaster_alerts/widgets/place_type_selector.dart b/lib/features/disaster_alerts/widgets/place_type_selector.dart index 3add794..a871745 100644 --- a/lib/features/disaster_alerts/widgets/place_type_selector.dart +++ b/lib/features/disaster_alerts/widgets/place_type_selector.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:lottie/lottie.dart'; import '../models/place_type.dart'; +import 'dart:ui'; -class PlaceTypeSelector extends StatelessWidget { +class PlaceTypeSelector extends StatefulWidget { final PlaceType selectedPlaceType; final Function(PlaceType) onTypeSelected; @@ -13,10 +14,96 @@ class PlaceTypeSelector extends StatelessWidget { required this.onTypeSelected, }) : super(key: key); + @override + State createState() => _PlaceTypeSelectorState(); +} + +class _PlaceTypeSelectorState extends State + with TickerProviderStateMixin { + late ScrollController _scrollController; + late AnimationController _selectionAnimationController; + late Animation _selectionAnimation; + + // Track item positions for spring physics + List _itemKeys = []; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + + // Initialize selection animation + _selectionAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 350), + ); + + _selectionAnimation = CurvedAnimation( + parent: _selectionAnimationController, + curve: Curves.easeOutCubic, + ); + + // Create keys for all place types + _itemKeys = List.generate( + PlaceType.values.length, + (index) => GlobalKey(), + ); + + // Trigger initial animation + _selectionAnimationController.forward(); + } + + @override + void didUpdateWidget(PlaceTypeSelector oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.selectedPlaceType != widget.selectedPlaceType) { + // Reset and play animation when selection changes + _selectionAnimationController.reset(); + _selectionAnimationController.forward(); + + // Scroll to selected item + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToSelectedItem(); + }); + } + } + + void _scrollToSelectedItem() { + final index = PlaceType.values.indexOf(widget.selectedPlaceType); + if (_scrollController.hasClients && + index >= 0 && + index < _itemKeys.length) { + // Get the context and RenderBox of the selected item + final itemContext = _itemKeys[index].currentContext; + if (itemContext != null) { + final RenderBox box = itemContext.findRenderObject() as RenderBox; + final position = box.localToGlobal(Offset.zero); + + // Calculate the center of the screen + final screenWidth = MediaQuery.of(context).size.width; + final screenCenter = screenWidth / 2; + + // Calculate how much to scroll to center the item + final itemCenter = position.dx + (box.size.width / 2); + final scrollAmount = + _scrollController.offset + (itemCenter - screenCenter); + + // Animate to the position + if (_scrollController.position.maxScrollExtent >= 0) { + _scrollController.animateTo( + scrollAmount.clamp(0.0, _scrollController.position.maxScrollExtent), + duration: const Duration(milliseconds: 350), + curve: Curves.easeOutQuart, + ); + } + } + } + } + String _getAnimationAsset(PlaceType type) { switch (type) { case PlaceType.hospital: - return 'assets/animations/hospital_ambulance.json'; // Updated animation + return 'assets/animations/hospital_ambulance.json'; case PlaceType.police: return 'assets/animations/police_bike.json'; case PlaceType.fire: @@ -28,85 +115,271 @@ class PlaceTypeSelector extends StatelessWidget { } } + @override + void dispose() { + _scrollController.dispose(); + _selectionAnimationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return SizedBox( - height: 100, - child: ListView.builder( - scrollDirection: Axis.horizontal, - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.symmetric(horizontal: 8), // Adjusted padding - itemCount: PlaceType.values.length, - itemBuilder: (context, index) { - final type = PlaceType.values[index]; - final isSelected = type == selectedPlaceType; - final isFirst = index == 0; - final isLast = index == PlaceType.values.length - 1; - - return AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeOutCubic, - margin: EdgeInsets.only( - left: isFirst ? 8 : 12, - right: isLast ? 8 : 12, - top: 8, - bottom: 8, - ), - child: Material( - color: isSelected ? Colors.blue.shade500 : Colors.white, - borderRadius: BorderRadius.circular(24), - elevation: isSelected ? 8 : 2, - shadowColor: isSelected ? Colors.blue.withOpacity(0.4) : Colors.black12, - child: InkWell( - onTap: () => onTypeSelected(type), - borderRadius: BorderRadius.circular(24), + height: 110, + child: AnimatedBuilder( + animation: _selectionAnimation, + builder: (context, _) { + final animationValue = _selectionAnimation.value; + + return Stack( + children: [ + // Main content with cards + ShaderMask( + shaderCallback: (Rect rect) { + return LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.black.withOpacity(0), + Colors.black, + Colors.black, + Colors.black.withOpacity(0), + ], + stops: const [0.0, 0.05, 0.95, 1.0], + ).createShader(rect); + }, + blendMode: BlendMode.dstIn, + child: ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: PlaceType.values.length, + itemBuilder: (context, index) { + final type = PlaceType.values[index]; + final isSelected = type == widget.selectedPlaceType; + + return SizedBox( + key: _itemKeys[index], + width: 150, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: _buildTypeCard( + type, + isSelected, + animationValue, + index == + PlaceType.values + .indexOf(widget.selectedPlaceType), + ), + ), + ); + }, + ), + ), + + // Edge gradients for smooth scrolling indication + Positioned( + left: 0, + top: 0, + bottom: 0, + width: 20, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - constraints: const BoxConstraints(minWidth: 160), // Added minimum width decoration: BoxDecoration( - border: Border.all( - color: isSelected ? Colors.blue.shade300 : Colors.grey.withOpacity(0.1), - width: 1.5, + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context) + .scaffoldBackgroundColor + .withOpacity(0.0), + ], ), - borderRadius: BorderRadius.circular(24), ), - child: Row( - mainAxisSize: MainAxisSize.min, // Added to prevent stretching - children: [ - Container( - width: 56, - height: 56, - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: isSelected ? Colors.white.withOpacity(0.2) : Colors.blue.withOpacity(0.05), - borderRadius: BorderRadius.circular(16), + ), + ), + Positioned( + right: 0, + top: 0, + bottom: 0, + width: 20, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerRight, + end: Alignment.centerLeft, + colors: [ + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context) + .scaffoldBackgroundColor + .withOpacity(0.0), + ], + ), + ), + ), + ), + ], + ); + }, + ), + ); + } + + Widget _buildTypeCard( + PlaceType type, bool isSelected, double animValue, bool isAnimating) { + // iOS-style design colors + final Color iosBlue = const Color(0xFF007AFF); + final Color iosBackground = Colors.white; + final Color iosShadow = const Color(0x1A000000); + + // Apply spring animation for currently animating item + final scale = isAnimating + ? Curves.easeOutBack.transform(animValue) * 0.05 + 0.95 + : 1.0; + + return GestureDetector( + onTap: () => widget.onTypeSelected(type), + child: Transform.scale( + scale: scale, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + decoration: BoxDecoration( + color: iosBackground, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: iosShadow, + blurRadius: isSelected ? 8 : 4, + offset: const Offset(0, 2), + spreadRadius: 0, + ), + ], + ), + clipBehavior: Clip.antiAlias, + child: Stack( + children: [ + // Background for selected state + if (isSelected) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + iosBlue, + iosBlue.withBlue(255), + ], + ), + ), + ), + ), + + // Frosted glass effect for iOS feel + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: isSelected + ? ImageFilter.blur(sigmaX: 0, sigmaY: 0) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: isSelected + ? Colors.white.withOpacity(0.2) + : Colors.grey.withOpacity(0.1), + width: 1, ), + ), + ), + ), + ), + ), + + // Content + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Animation container with iOS-style glass background + Container( + width: 54, + height: 54, + decoration: BoxDecoration( + color: isSelected + ? Colors.white.withOpacity(0.25) + : const Color(0xFFF0F7FF), + borderRadius: BorderRadius.circular(20), + boxShadow: isSelected + ? null + : [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + spreadRadius: 0, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), child: Lottie.asset( _getAnimationAsset(type), fit: BoxFit.contain, animate: isSelected, ), ), - const SizedBox(width: 16), - Flexible( - child: Text( - type.label, - style: GoogleFonts.inter( - color: isSelected ? Colors.white : Colors.black87, - fontWeight: FontWeight.w600, - fontSize: 16, - letterSpacing: 0.5, - ), - ), + ), + const SizedBox(height: 8), + + // Type label with SF Pro-like styling + Text( + type.label, + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w500, + color: + isSelected ? Colors.white : const Color(0xFF333333), + letterSpacing: -0.3, ), - ], - ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), ), - ), - ); - }, + + // Selection indicator for iOS feel + if (isSelected && isAnimating) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 350), + curve: Curves.easeOutQuart, + builder: (context, value, child) { + return Container( + height: 4, + width: double.infinity, + color: Colors.white, + margin: + EdgeInsets.symmetric(horizontal: 20 * (1 - value)), + ); + }, + ), + ), + ], + ), + ), ), ); } -} \ No newline at end of file +} diff --git a/lib/shared/widgets/chat_assistance.dart b/lib/shared/widgets/chat_assistance.dart index 7e5dda9..c6938e1 100644 --- a/lib/shared/widgets/chat_assistance.dart +++ b/lib/shared/widgets/chat_assistance.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:disaster_management/core/constants/app_colors.dart'; import 'package:disaster_management/shared/controllers/chat_assistance_controller.dart'; import 'package:disaster_management/shared/models/chat_message.dart'; +import 'package:flutter/services.dart'; +import 'dart:async'; class ChatAssistance extends StatefulWidget { const ChatAssistance({super.key}); @@ -10,9 +12,61 @@ class ChatAssistance extends StatefulWidget { State createState() => _ChatAssistanceState(); } -class _ChatAssistanceState extends State with SingleTickerProviderStateMixin { +class _ChatAssistanceState extends State + with SingleTickerProviderStateMixin { late ChatAssistanceController _controller; late AnimationController _animationController; + bool _isSpeaking = false; + bool _isMicActive = false; + + // Audio feedback + Future _playSound(String soundType) async { + try { + await SystemSound.play(soundType == 'success' + ? SystemSoundType.click + : SystemSoundType.alert); + } catch (e) { + // Silently handle errors for devices that don't support sound + } + } + + void _toggleMic() { + setState(() { + _isMicActive = !_isMicActive; + }); + + // Play sound feedback + _playSound('success'); + + if (_isMicActive) { + // Here you would implement actual voice recognition + // For now, we'll simulate it with a timer + Timer(const Duration(seconds: 3), () { + if (mounted && _isMicActive) { + setState(() { + _isMicActive = false; + }); + + // Simulate sending a voice message + _controller.messageController.text = + "Help me find evacuation routes nearby"; + _controller.sendMessage(); + } + }); + } + } + + void _toggleSpeaker() { + setState(() { + _isSpeaking = !_isSpeaking; + }); + + // Play sound feedback + _playSound('success'); + + // Here you would implement TTS for the last message + // This is just a placeholder for the actual implementation + } @override void initState() { @@ -63,19 +117,33 @@ class _ChatAssistanceState extends State with SingleTickerProvid Widget build(BuildContext context) { // Get screen size for responsive layout final Size screenSize = MediaQuery.of(context).size; + final bool isSmallScreen = screenSize.width < 360; + + // Adaptive sizing based on screen size final double chatWidth = screenSize.width < 600 - ? screenSize.width * 0.85 + ? isSmallScreen + ? screenSize.width * 0.95 + : screenSize.width * 0.85 : 400; - final double chatHeight = screenSize.height < 700 + final double chatHeight = screenSize.height < 500 + ? screenSize.height * 0.7 + : screenSize.height < 700 ? screenSize.height * 0.6 - : screenSize.height * 0.7 > 600 ? 600 : screenSize.height * 0.7; + : screenSize.height * 0.7 > 600 + ? 600 + : screenSize.height * 0.7; + + // Adjust padding for small screens + final double horizontalPadding = isSmallScreen ? 8.0 : 16.0; + final double verticalPadding = isSmallScreen ? 8.0 : 16.0; // Use SafeArea to avoid system UI overlaps return SafeArea( child: Align( alignment: Alignment.bottomRight, child: Padding( - padding: const EdgeInsets.only(right: 16, bottom: 16), + padding: EdgeInsets.only( + right: horizontalPadding, bottom: verticalPadding), child: Stack( clipBehavior: Clip.none, // Allow widgets to overflow alignment: Alignment.bottomRight, @@ -96,9 +164,10 @@ class _ChatAssistanceState extends State with SingleTickerProvid borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - spreadRadius: 1, + color: Colors.black.withOpacity(0.15), + blurRadius: 15, + spreadRadius: 2, + offset: const Offset(0, 4), ), ], ), @@ -110,27 +179,34 @@ class _ChatAssistanceState extends State with SingleTickerProvid // Chat Messages Expanded( child: ValueListenableBuilder( - valueListenable: _controller.messagesNotifier, - builder: (context, List messages, _) { - return _buildMessageList(chatWidth, messages); + valueListenable: + _controller.messagesNotifier, + builder: (context, + List messages, _) { + return _buildMessageList( + chatWidth, messages, isSmallScreen); }, ), ), // Loading indicator ValueListenableBuilder( - valueListenable: _controller.isLoadingNotifier, + valueListenable: + _controller.isLoadingNotifier, builder: (context, isLoading, _) { return AnimatedContainer( - duration: const Duration(milliseconds: 200), + duration: + const Duration(milliseconds: 200), height: isLoading ? 40 : 0, child: isLoading ? const Center( child: SizedBox( width: 20, height: 20, - child: CircularProgressIndicator( + child: + CircularProgressIndicator( strokeWidth: 2, + color: AppColors.primaryColor, ), ), ) @@ -140,7 +216,7 @@ class _ChatAssistanceState extends State with SingleTickerProvid ), // Input Field - _buildInputField(), + _buildInputField(isSmallScreen), ], ), ) @@ -164,15 +240,20 @@ class _ChatAssistanceState extends State with SingleTickerProvid curve: Curves.elasticOut, )), child: Padding( - padding: const EdgeInsets.only(bottom: 70), // Added space for the button + padding: const EdgeInsets.only( + bottom: 70), // Added space for the button child: ConstrainedBox( constraints: BoxConstraints( - maxWidth: chatWidth, + maxWidth: isSmallScreen + ? screenSize.width * 0.85 + : chatWidth, ), child: Material( color: Colors.transparent, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: EdgeInsets.symmetric( + horizontal: isSmallScreen ? 12 : 16, + vertical: isSmallScreen ? 8 : 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), @@ -184,13 +265,65 @@ class _ChatAssistanceState extends State with SingleTickerProvid ), ], ), - child: Row( + child: isSmallScreen + ? Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Icon(Icons.radar, + color: AppColors.primaryColor, + size: 16), + const SizedBox(width: 6), + const Expanded( + child: Text( + 'Check disaster risks in your area!', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 12, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + _animationController.reverse(); + _controller.isExpandedNotifier + .value = true; + _controller.checkDisasterRisks(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: + AppColors.primaryColor, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), + minimumSize: Size.zero, + ), + child: const Text( + 'Check Now', + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + ), + ], + ) + : Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.radar, color: AppColors.primaryColor), + Icon(Icons.radar, + color: AppColors.primaryColor), const SizedBox(width: 8), - Flexible( - child: const Text( + const Flexible( + child: Text( 'Check disaster risks\nin your area!', style: TextStyle( fontWeight: FontWeight.w500, @@ -201,12 +334,15 @@ class _ChatAssistanceState extends State with SingleTickerProvid ElevatedButton( onPressed: () { _animationController.reverse(); - _controller.isExpandedNotifier.value = true; + _controller.isExpandedNotifier.value = + true; _controller.checkDisasterRisks(); }, style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryColor, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + backgroundColor: + AppColors.primaryColor, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), minimumSize: Size.zero, ), child: const Text( @@ -227,7 +363,7 @@ class _ChatAssistanceState extends State with SingleTickerProvid }, ), - // Chat Button + // Chat Button with pulse effect Material( color: AppColors.primaryColor, borderRadius: BorderRadius.circular(30), @@ -237,16 +373,35 @@ class _ChatAssistanceState extends State with SingleTickerProvid builder: (context, isExpanded, _) { return InkWell( onTap: () { - _controller.isExpandedNotifier.value = !_controller.isExpandedNotifier.value; + _controller.isExpandedNotifier.value = + !_controller.isExpandedNotifier.value; + _playSound('success'); }, borderRadius: BorderRadius.circular(30), - child: SizedBox( + child: Container( width: 56, height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppColors.primaryColor, + AppColors.primaryColor.withBlue( + (AppColors.primaryColor.blue + 40) + .clamp(0, 255)), + ], + ), + ), child: Center( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), child: Icon( isExpanded ? Icons.close : Icons.support_agent, + key: ValueKey(isExpanded), color: Colors.white, + ), ), ), ), @@ -261,286 +416,201 @@ class _ChatAssistanceState extends State with SingleTickerProvid ); } - // Extract chat window to a separate method to reduce rebuilds - Widget _buildChatWindow(double width, double height) { - return Positioned( - bottom: 70, // Position above the chat button - right: 0, - child: Material( - elevation: 10, - borderRadius: BorderRadius.circular(20), - color: Colors.white, - child: SizedBox( - width: width, - height: height, - child: Column( - children: [ - // Chat Header - _buildChatHeader(), - - // Chat Messages - Expanded( - child: ValueListenableBuilder( - valueListenable: _controller.messagesNotifier, - builder: (context, List messages, _) { - return _buildMessageList(width, messages); - }, - ), - ), - - // Loading indicator - ValueListenableBuilder( - valueListenable: _controller.isLoadingNotifier, - builder: (context, isLoading, _) { - return AnimatedContainer( - duration: const Duration(milliseconds: 200), - height: isLoading ? 40 : 0, - child: isLoading - ? const Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ), - ) - : const SizedBox.shrink(), - ); - }, - ), - - // Input Field - _buildInputField(), - ], - ), + Widget _buildChatHeader() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: AppColors.primaryColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), - ); - } + child: Row( + children: [ + // Header icon + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.support_agent, + color: Colors.white, + size: 18, + ), + ), + const SizedBox(width: 12), - Widget _buildChatButton() { - return ValueListenableBuilder( - valueListenable: _controller.isExpandedNotifier, - builder: (context, isExpanded, _) { - return Positioned( - bottom: 0, - right: 0, - child: Material( - color: AppColors.primaryColor, - borderRadius: BorderRadius.circular(30), - elevation: 5, - child: InkWell( - onTap: () { - _controller.isExpandedNotifier.value = !_controller.isExpandedNotifier.value; - }, - child: SizedBox( - width: 56, - height: 56, - child: Center( - child: Icon( - isExpanded ? Icons.close : Icons.support_agent, + // Header text + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Disaster Assistant', + style: TextStyle( color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16, ), ), - ), + Text( + 'Ask me about safety information', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 12, + ), + ), + ], ), ), - ); - }, + + // Audio button + IconButton( + icon: Icon( + _isSpeaking ? Icons.volume_up : Icons.volume_off, + color: Colors.white, + size: 20, + ), + onPressed: _toggleSpeaker, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ), + ], + ), ); } - Widget _buildMessageList(double chatWidth, List messages) { + Widget _buildMessageList( + double chatWidth, List messages, bool isSmallScreen) { + final double maxBubbleWidth = + isSmallScreen ? chatWidth * 0.8 : chatWidth * 0.7; + return ListView.builder( controller: _controller.scrollController, - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + padding: EdgeInsets.symmetric( + vertical: isSmallScreen ? 12 : 16, + horizontal: isSmallScreen ? 12 : 16), itemCount: messages.length, itemBuilder: (context, index) { final message = messages[index]; return _MessageBubble( message: message, - maxWidth: chatWidth * 0.7, + maxWidth: maxBubbleWidth, + isSmallScreen: isSmallScreen, key: ValueKey('message_$index'), ); }, ); } - Widget _buildChatHeader() { + Widget _buildInputField(bool isSmallScreen) { + final double iconSize = isSmallScreen ? 20.0 : 24.0; + final double padding = isSmallScreen ? 8.0 : 12.0; + return Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + padding: EdgeInsets.symmetric(horizontal: padding, vertical: padding / 2), decoration: BoxDecoration( - color: AppColors.primaryColor, - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + color: Colors.grey.shade50, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + border: Border( + top: BorderSide( + color: Colors.grey.shade200, + width: 1, + ), + ), ), child: Row( children: [ - const CircleAvatar( - backgroundColor: Colors.white, - radius: 16, - child: Icon( - Icons.support_agent, - color: Colors.blue, // Use direct color to avoid AppColors lookup - size: 18, + // Microphone button + Container( + decoration: BoxDecoration( + color: _isMicActive + ? AppColors.primaryColor.withOpacity(0.1) + : Colors.transparent, + shape: BoxShape.circle, ), - ), - const SizedBox(width: 12), - const Text( - 'Emergency Assistant', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - // Add AI prediction button - IconButton( - icon: const Icon(Icons.radar, color: Colors.white), - tooltip: 'Predict Disasters', - onPressed: () { - _showDisasterPredictionDialog(); - }, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close, color: Colors.white), - onPressed: () { - _controller.isExpandedNotifier.value = false; - }, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ], - ), - ); - } - - // Add this method to show the disaster prediction dialog - void _showDisasterPredictionDialog() { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Row( - children: [ - Icon(Icons.warning_amber_rounded, color: Colors.orange), - SizedBox(width: 10), - Text('AI Disaster Prediction'), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Would you like to check potential disaster risks in your current location using AI?', - style: TextStyle(fontSize: 16), - ), - SizedBox(height: 16), - Container( - padding: EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), + child: IconButton( + icon: Icon( + _isMicActive ? Icons.mic : Icons.mic_none, + color: _isMicActive + ? AppColors.primaryColor + : Colors.grey.shade600, + size: iconSize, ), - child: Row( - children: [ - Icon(Icons.location_on, color: Colors.blue), - SizedBox(width: 8), - Expanded( - child: Text( - 'This will use your current location to analyze potential risks based on historical data and environmental factors.', - style: TextStyle( - fontSize: 14, - color: Colors.blue.shade800, - ), - ), - ), - ], + onPressed: _toggleMic, + padding: EdgeInsets.zero, + constraints: BoxConstraints( + minWidth: iconSize + 8, + minHeight: iconSize + 8, ), ), - ], - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('Cancel'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - _controller.isExpandedNotifier.value = true; - _controller.checkDisasterRisks(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryColor, - ), - child: Text('Check Risks', style: TextStyle(color: Colors.white)), ), - ], - ), - ); - } - - - Widget _buildInputField() { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.vertical( - bottom: Radius.circular(20), - ), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - blurRadius: 5, - spreadRadius: 1, - offset: const Offset(0, -2), - ), - ], - ), - child: Row( - children: [ + // Text field Expanded( child: TextField( controller: _controller.messageController, decoration: InputDecoration( - hintText: 'Type your emergency...', - hintStyle: TextStyle(color: Colors.grey.shade400), - filled: true, - fillColor: Colors.grey.shade100, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(30), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, + hintText: 'Type your message...', + hintStyle: TextStyle( + color: Colors.grey.shade400, + fontSize: isSmallScreen ? 13 : 14, ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: padding, vertical: padding / 2), + ), + style: TextStyle( + fontSize: isSmallScreen ? 13 : 14, ), - onSubmitted: (_) => _controller.sendMessage(), + onSubmitted: (text) { + if (text.trim().isNotEmpty) { + _controller.sendMessage(); + } + }, ), ), - const SizedBox(width: 8), - Material( + + // Send button + Container( + decoration: BoxDecoration( color: AppColors.primaryColor, - borderRadius: BorderRadius.circular(30), - child: InkWell( - onTap: () => _controller.sendMessage(), - borderRadius: BorderRadius.circular(30), - child: const Padding( - padding: EdgeInsets.all(10), - child: Icon( - Icons.send, + shape: BoxShape.circle, + ), + child: IconButton( + icon: Icon( + Icons.send_rounded, color: Colors.white, - size: 20, - ), + size: iconSize * 0.8, + ), + onPressed: () { + final text = _controller.messageController.text.trim(); + if (text.isNotEmpty) { + _controller.sendMessage(); + _playSound('success'); + } + }, + padding: EdgeInsets.zero, + constraints: BoxConstraints( + minWidth: iconSize + 8, + minHeight: iconSize + 8, ), ), ), @@ -550,55 +620,69 @@ class _ChatAssistanceState extends State with SingleTickerProvid } } -// Extract message bubble to a separate stateless widget to reduce rebuilds class _MessageBubble extends StatelessWidget { final ChatMessage message; final double maxWidth; + final bool isSmallScreen; const _MessageBubble({ required this.message, required this.maxWidth, + this.isSmallScreen = false, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { + final double fontSize = isSmallScreen ? 12.0 : 14.0; + final double padding = isSmallScreen ? 8.0 : 10.0; + final double avatarSize = isSmallScreen ? 28.0 : 32.0; + return Padding( - padding: const EdgeInsets.only(bottom: 12), + padding: EdgeInsets.only( + bottom: isSmallScreen ? 8 : 12, + left: message.isUser ? avatarSize : 0, + right: message.isUser ? 0 : avatarSize, + ), child: Row( - mainAxisAlignment: message.isUser - ? MainAxisAlignment.end - : MainAxisAlignment.start, + mainAxisAlignment: + message.isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!message.isUser) CircleAvatar( backgroundColor: AppColors.primaryColor.withOpacity(0.1), - radius: 16, + radius: avatarSize / 2, child: Icon( Icons.support_agent, color: AppColors.primaryColor, - size: 16, + size: avatarSize / 2, ), ), - if (!message.isUser) const SizedBox(width: 8), - + if (!message.isUser) SizedBox(width: isSmallScreen ? 6 : 8), Flexible( child: Container( constraints: BoxConstraints( maxWidth: maxWidth, ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, + padding: EdgeInsets.symmetric( + horizontal: padding + 4, + vertical: padding, ), decoration: BoxDecoration( color: message.isUser ? AppColors.primaryColor : message.isError ? Colors.red.shade50 - : Colors.grey.shade200, + : Colors.grey.shade100, borderRadius: BorderRadius.circular(18), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], ), child: SelectableText( message.text, @@ -608,25 +692,24 @@ class _MessageBubble extends StatelessWidget { : message.isError ? Colors.red.shade800 : Colors.black87, - fontSize: 14, + fontSize: fontSize, ), ), ), ), - - if (message.isUser) const SizedBox(width: 8), + if (message.isUser) SizedBox(width: isSmallScreen ? 6 : 8), if (message.isUser) - const CircleAvatar( - backgroundColor: Colors.blue, - radius: 16, + CircleAvatar( + backgroundColor: Colors.blue.shade100, + radius: avatarSize / 2, child: Icon( Icons.person, - color: Colors.white, - size: 16, + color: AppColors.primaryColor, + size: avatarSize / 2, ), ), ], ), ); } -} \ No newline at end of file +}