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

352 lines
11 KiB
Dart

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../constants.dart';
import '../models/weather_models.dart';
class WeatherDetailsSheet extends StatefulWidget {
final Forecast forecast;
final CurrentWeather current;
const WeatherDetailsSheet({
super.key,
required this.forecast,
required this.current,
});
@override
State<WeatherDetailsSheet> createState() => _WeatherDetailsSheetState();
}
class _WeatherDetailsSheetState extends State<WeatherDetailsSheet> {
late DraggableScrollableController _sheetController;
@override
void initState() {
super.initState();
_sheetController = DraggableScrollableController();
}
@override
void dispose() {
_sheetController.dispose();
super.dispose();
}
Future<void> _animateSheetUp() async {
if (!_sheetController.isAttached) return;
final currentSize = _sheetController.size;
double targetSize;
// Determine target size based on current position
if (currentSize < 0.4) {
// If small, expand to medium (0.6)
targetSize = 0.6;
} else if (currentSize < 0.75) {
// If medium, expand to large (0.9)
targetSize = 0.9;
} else {
// If large, collapse to small (0.2)
targetSize = 0.2;
}
await _sheetController.animateTo(
targetSize,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
);
}
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
controller: _sheetController,
initialChildSize: 0.2,
minChildSize: 0.15,
maxChildSize: 0.9,
builder: (BuildContext context, ScrollController scrollController) {
return ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15.0, sigmaY: 15.0),
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.4),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
),
child: Column(
children: [
// Clickable header area
GestureDetector(
onTap: _animateSheetUp,
behavior: HitTestBehavior.opaque,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
Center(
child: Container(
width: 40,
height: 5,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.4),
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.keyboard_arrow_up,
color: Colors.white.withOpacity(0.7),
size: 20,
),
const SizedBox(width: 8),
Text(
'Tap to expand',
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
),
// Scrollable content
Expanded(
child: ListView(
controller: scrollController,
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
children: [
const SizedBox(height: 8),
_buildSectionTitle('Hourly Forecast'),
SizedBox(
height: 140,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount:
widget.forecast.forecastday[0].hour.length,
itemBuilder: (context, index) {
final hour =
widget.forecast.forecastday[0].hour[index];
if (DateTime.parse(
hour.time,
).isBefore(DateTime.now())) {
return const SizedBox.shrink();
}
return _HourlyForecastItem(hour: hour);
},
),
),
const Divider(color: Colors.white24, height: 32),
_buildSectionTitle('7-Day Forecast'),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: widget.forecast.forecastday.length,
itemBuilder: (context, index) {
final day = widget.forecast.forecastday[index];
return _DailyForecastItem(day: day);
},
),
const Divider(color: Colors.white24, height: 32),
_buildSectionTitle('Current Details'),
_buildDetailsGrid(),
],
),
),
],
),
),
),
);
},
);
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
title.toUpperCase(),
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 14,
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
),
),
);
}
Widget _buildDetailsGrid() {
final astro = widget.forecast.forecastday[0].astro;
return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
childAspectRatio: 2.1,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: [
_DetailItem(
title: 'Feels Like',
value: '${widget.current.feelslikeC.round()}°',
),
_DetailItem(title: 'UV Index', value: '${widget.current.uv.round()}'),
_DetailItem(title: 'Humidity', value: '${widget.current.humidity}%'),
_DetailItem(
title: 'Wind',
value: '${widget.current.windKph.round()} km/h',
),
_DetailItem(title: 'Sunrise', value: astro.sunrise),
_DetailItem(title: 'Sunset', value: astro.sunset),
],
);
}
}
class _HourlyForecastItem extends StatelessWidget {
final Hour hour;
const _HourlyForecastItem({required this.hour});
@override
Widget build(BuildContext context) {
final time = DateFormat.j().format(DateTime.parse(hour.time));
return Container(
width: 80,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: kCardBackground,
borderRadius: kCardBorderRadius,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
time,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Image.network(hour.condition.icon, width: 40, height: 40),
Text(
'${hour.tempC.round()}°',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}
class _DailyForecastItem extends StatelessWidget {
final ForecastDay day;
const _DailyForecastItem({required this.day});
@override
Widget build(BuildContext context) {
final date = DateTime.parse(day.date);
final dayName = DateFormat.E().format(date);
final isToday =
DateFormat.yMd().format(date) ==
DateFormat.yMd().format(DateTime.now());
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isToday ? 'Today' : dayName,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
Image.network(day.day.condition.icon, width: 30, height: 30),
Text(
'${day.day.dailyChanceOfRain}%',
style: TextStyle(
color: Colors.blue.shade200,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
Text(
'H: ${day.day.maxtempC.round()}° L: ${day.day.mintempC.round()}°',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}
class _DetailItem extends StatelessWidget {
final String title;
final String value;
const _DetailItem({required this.title, required this.value});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: kCardBackground,
borderRadius: kCardBorderRadius,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title.toUpperCase(),
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}