AtmoSphere/lib/screens/map_screen.dart
2026-01-16 18:22:32 +08:00

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();
}
}