586 lines
20 KiB
Dart
586 lines
20 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
import 'package:latlong2/latlong.dart';
|
|
import 'package:geolocator/geolocator.dart';
|
|
import '../constants.dart';
|
|
import '../providers.dart';
|
|
import '../providers/main_screen_provider.dart';
|
|
import '../tools/app_ads_managers.dart';
|
|
|
|
class MapScreen extends ConsumerStatefulWidget {
|
|
const MapScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<MapScreen> createState() => _MapScreenState();
|
|
}
|
|
|
|
class _MapScreenState extends ConsumerState<MapScreen>
|
|
with SingleTickerProviderStateMixin {
|
|
final MapController _mapController = MapController();
|
|
LatLng _selectedLocation = const LatLng(
|
|
40.7128,
|
|
-74.0060,
|
|
); // Default: New York
|
|
bool _isLoading = false;
|
|
bool _isLocating = false;
|
|
late AnimationController _pulseController;
|
|
late Animation<double> _pulseAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_pulseController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 1500),
|
|
)..repeat(reverse: true);
|
|
_pulseAnimation = Tween<double>(begin: 0.8, end: 1.2).animate(
|
|
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
|
|
);
|
|
// Initialize map to New York
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_mapController.move(_selectedLocation, 10.0);
|
|
});
|
|
}
|
|
|
|
Future<void> _getCurrentLocation() async {
|
|
setState(() {
|
|
_isLocating = true;
|
|
});
|
|
|
|
try {
|
|
// Check if location services are enabled
|
|
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
|
if (!serviceEnabled) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: const Text(
|
|
'Location services are disabled. Please enable them in settings.',
|
|
),
|
|
backgroundColor: Colors.orange,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check location permissions
|
|
LocationPermission permission = await Geolocator.checkPermission();
|
|
if (permission == LocationPermission.denied) {
|
|
permission = await Geolocator.requestPermission();
|
|
if (permission == LocationPermission.denied) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: const Text('Location permissions are denied.'),
|
|
backgroundColor: Colors.orange,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (permission == LocationPermission.deniedForever) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: const Text(
|
|
'Location permissions are permanently denied. Please enable them in app settings.',
|
|
),
|
|
backgroundColor: Colors.orange,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Get current position
|
|
Position position = await Geolocator.getCurrentPosition(
|
|
desiredAccuracy: LocationAccuracy.high,
|
|
);
|
|
|
|
final newLocation = LatLng(position.latitude, position.longitude);
|
|
|
|
setState(() {
|
|
_selectedLocation = newLocation;
|
|
});
|
|
|
|
// Animate map to current location
|
|
_mapController.move(newLocation, 15.0);
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: const Row(
|
|
children: [
|
|
Icon(Icons.my_location, color: Colors.white, size: 20),
|
|
SizedBox(width: 8),
|
|
Text('Location updated to current position'),
|
|
],
|
|
),
|
|
backgroundColor: Colors.green,
|
|
duration: const Duration(seconds: 2),
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Failed to get location: $e'),
|
|
backgroundColor: Colors.red,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLocating = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void _onMapMoved() {
|
|
final center = _mapController.camera.center;
|
|
setState(() {
|
|
_selectedLocation = center;
|
|
});
|
|
}
|
|
|
|
Future<void> _confirmLocation() async {
|
|
|
|
InterstitialAdManager.instance.showAd(InterstitialAdType.second,onAdCompleted: (){
|
|
|
|
});
|
|
|
|
setState(() {
|
|
_isLoading = true;
|
|
});
|
|
|
|
try {
|
|
final coordinateQuery =
|
|
'${_selectedLocation.latitude},${_selectedLocation.longitude}';
|
|
|
|
final weatherNotifier = ref.read(weatherProvider.notifier);
|
|
await weatherNotifier.switchCity(coordinateQuery);
|
|
|
|
if (mounted) {
|
|
switchToHome(ref);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: const Row(
|
|
children: [
|
|
Icon(Icons.check_circle, color: Colors.white),
|
|
SizedBox(width: 8),
|
|
Text('Weather information retrieved successfully'),
|
|
],
|
|
),
|
|
backgroundColor: Colors.green,
|
|
duration: const Duration(seconds: 2),
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Failed to fetch weather: $e'),
|
|
backgroundColor: Colors.red,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
kDarkPurple,
|
|
kDarkPurple.withOpacity(0.95),
|
|
Colors.black.withOpacity(0.9),
|
|
],
|
|
stops: const [0.0, 0.5, 1.0],
|
|
),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
FlutterMap(
|
|
mapController: _mapController,
|
|
options: MapOptions(
|
|
initialCenter: _selectedLocation,
|
|
initialZoom: 10.0,
|
|
onMapEvent: (event) {
|
|
if (event is MapEventMove) {
|
|
_onMapMoved();
|
|
}
|
|
},
|
|
),
|
|
children: [
|
|
TileLayer(
|
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
userAgentPackageName: 'com.example.atmo_sphere',
|
|
),
|
|
],
|
|
),
|
|
// Center location pin with pulse animation
|
|
Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
AnimatedBuilder(
|
|
animation: _pulseAnimation,
|
|
builder: (context, child) {
|
|
return Transform.scale(
|
|
scale: _pulseAnimation.value,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.red.withOpacity(0.4),
|
|
blurRadius: 20,
|
|
spreadRadius: 5,
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.3),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(
|
|
Icons.location_on,
|
|
color: Colors.red,
|
|
size: 48,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 10,
|
|
),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.black.withOpacity(0.9),
|
|
Colors.black.withOpacity(0.8),
|
|
],
|
|
),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(
|
|
color: Colors.white.withOpacity(0.2),
|
|
width: 1,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.4),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.my_location,
|
|
size: 14,
|
|
color: Colors.blueAccent.withOpacity(0.9),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
'Coordinates',
|
|
style: TextStyle(
|
|
color: Colors.white.withOpacity(0.7),
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w500,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'${_selectedLocation.latitude.toStringAsFixed(4)}, ${_selectedLocation.longitude.toStringAsFixed(4)}',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 0.5,
|
|
fontFeatures: [FontFeature.tabularFigures()],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// Location button (top right)
|
|
Positioned(
|
|
top: MediaQuery.of(context).padding.top + 20,
|
|
right: 16,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.greenAccent.withOpacity(0.4),
|
|
blurRadius: 15,
|
|
spreadRadius: 2,
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.3),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(16),
|
|
onTap: _isLocating ? null : _getCurrentLocation,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.greenAccent.withOpacity(0.9),
|
|
Colors.greenAccent.withOpacity(0.8),
|
|
],
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: Colors.white.withOpacity(0.2),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: _isLocating
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2.5,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Colors.white,
|
|
),
|
|
),
|
|
)
|
|
: const Icon(
|
|
Icons.my_location,
|
|
color: Colors.white,
|
|
size: 24,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Top info card
|
|
Positioned(
|
|
top: MediaQuery.of(context).padding.top + 20,
|
|
left: 16,
|
|
right: 80, // Leave space for location button
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 20,
|
|
vertical: 16,
|
|
),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.black.withOpacity(0.85),
|
|
Colors.black.withOpacity(0.75),
|
|
],
|
|
),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(
|
|
color: Colors.white.withOpacity(0.1),
|
|
width: 1,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.4),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 6),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blueAccent.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: const Icon(
|
|
Icons.info_outline,
|
|
color: Colors.blueAccent,
|
|
size: 22,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text(
|
|
'Select Location',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Drag the map to choose a location, then tap confirm to get weather',
|
|
style: TextStyle(
|
|
color: Colors.white.withOpacity(0.8),
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
height: 1.3,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Bottom confirm button
|
|
Positioned(
|
|
bottom: MediaQuery.of(context).padding.bottom + 24,
|
|
left: 16,
|
|
right: 16,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.blueAccent.withOpacity(0.4),
|
|
blurRadius: 20,
|
|
spreadRadius: 2,
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.3),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 6),
|
|
),
|
|
],
|
|
),
|
|
child: ElevatedButton(
|
|
onPressed: _isLoading ? null : _confirmLocation,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blueAccent,
|
|
foregroundColor: Colors.white,
|
|
disabledBackgroundColor: Colors.blueAccent.withOpacity(0.6),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 32,
|
|
vertical: 18,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
elevation: 0,
|
|
),
|
|
child: _isLoading
|
|
? const SizedBox(
|
|
width: 24,
|
|
height: 24,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2.5,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Colors.white,
|
|
),
|
|
),
|
|
)
|
|
: const Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.check_circle_outline, size: 26),
|
|
SizedBox(width: 10),
|
|
Text(
|
|
'Confirm',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
letterSpacing: 0.8,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_pulseController.dispose();
|
|
_mapController.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|