390 lines
15 KiB
Dart

import 'dart:convert';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:manager_api_new/api.dart';
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
import 'package:mymuseum_visitapp/Models/weatherData.dart';
import 'package:mymuseum_visitapp/app_context.dart';
import 'package:mymuseum_visitapp/constants.dart';
import 'package:provider/provider.dart';
class _DaySummary {
final int dt;
final List<WeatherForecast> forecasts;
final WeatherForecast representative;
final double minTemp;
final double maxTemp;
_DaySummary({
required this.dt,
required this.forecasts,
required this.representative,
required this.minTemp,
required this.maxTemp,
});
}
class WeatherPage extends StatefulWidget {
final WeatherDTO section;
const WeatherPage({super.key, required this.section});
@override
State<WeatherPage> createState() => _WeatherPageState();
}
class _WeatherPageState extends State<WeatherPage> {
WeatherDTO weatherDTO = WeatherDTO();
WeatherData? weatherData;
List<_DaySummary> _days = [];
int _selectedDayIndex = 0;
@override
void initState() {
super.initState();
weatherDTO = widget.section;
if (weatherDTO.result != null) {
weatherData = WeatherData.fromJson(jsonDecode(weatherDTO.result!));
_days = _buildDays(weatherData!.list!);
}
}
List<_DaySummary> _buildDays(List<WeatherForecast> allForecasts) {
final Map<int, List<WeatherForecast>> byDay = {};
for (final f in allForecasts) {
final date = DateTime.fromMillisecondsSinceEpoch(f.dt! * 1000);
final key = DateTime(date.year, date.month, date.day).millisecondsSinceEpoch;
byDay.putIfAbsent(key, () => []).add(f);
}
return byDay.entries.take(6).map((entry) {
final forecasts = entry.value;
final representative = forecasts.firstWhere(
(f) => DateTime.fromMillisecondsSinceEpoch(f.dt! * 1000).hour == 12,
orElse: () => forecasts.last,
);
final minTemp = forecasts.map((f) => f.main!.tempMin!).reduce((a, b) => a < b ? a : b);
final maxTemp = forecasts.map((f) => f.main!.tempMax!).reduce((a, b) => a > b ? a : b);
return _DaySummary(
dt: entry.key ~/ 1000,
forecasts: forecasts,
representative: representative,
minTemp: minTemp,
maxTemp: maxTemp,
);
}).toList();
}
String _shortDayLabel(int dt, AppContext appContext) {
final now = DateTime.now();
final date = DateTime.fromMillisecondsSinceEpoch(dt * 1000);
if (date.day == now.day && date.month == now.month) {
final lang = appContext.getContext().language?.toString().toUpperCase() ?? 'FR';
return lang == 'EN' ? 'Today' : lang == 'NL' ? 'Vandaag' : lang == 'DE' ? 'Heute' : 'Auj.';
}
return _weekdayName(date.weekday, appContext, short: true);
}
String _fullDayLabel(int dt, AppContext appContext) {
final now = DateTime.now();
final date = DateTime.fromMillisecondsSinceEpoch(dt * 1000);
final lang = appContext.getContext().language?.toString().toUpperCase() ?? 'FR';
if (date.day == now.day && date.month == now.month) {
return lang == 'EN' ? 'Today' : lang == 'NL' ? 'Vandaag' : lang == 'DE' ? 'Heute' : "Aujourd'hui";
}
final dayName = _weekdayName(date.weekday, appContext, short: false);
final locale = lang == 'EN' ? 'en_US' : lang == 'NL' ? 'nl_NL' : lang == 'DE' ? 'de_DE' : 'fr_FR';
try {
final monthDay = lang == 'EN'
? DateFormat('MMMM d', locale).format(date)
: DateFormat('d MMMM', locale).format(date);
return '$dayName $monthDay';
} catch (_) {
return '$dayName ${date.day}/${date.month}';
}
}
String _weekdayName(int weekday, AppContext appContext, {required bool short}) {
final ctx = appContext.getContext();
final keys = {
1: 'monday',
2: 'tuesday',
3: 'wednesday',
4: 'thursday',
5: 'friday',
6: 'saturday',
7: 'sunday',
};
final full = TranslationHelper.getFromLocale(keys[weekday]!, ctx);
return short && full.length > 3 ? '${full.substring(0, 3)}.' : full;
}
@override
Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context);
final visitAppContext = appContext.getContext();
final primaryColor = visitAppContext.configuration?.primaryColor != null
? Color(int.parse(
visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0],
radix: 16))
: kSecondColor;
final roundedValue =
visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0;
return Stack(
children: [
weatherData == null || _days.isEmpty
? const Center(child: Text("Aucune donnée météo"))
: _buildContent(appContext, primaryColor, roundedValue),
Positioned(
top: 35,
left: 10,
child: SizedBox(
width: 50,
height: 50,
child: InkWell(
onTap: () => Navigator.of(context).pop(),
child: Container(
decoration: BoxDecoration(color: primaryColor, shape: BoxShape.circle),
child: const Icon(Icons.arrow_back, size: 23, color: Colors.white),
),
),
),
),
],
);
}
Widget _buildContent(AppContext appContext, Color primaryColor, double roundedValue) {
final selected = _days[_selectedDayIndex];
final rep = selected.representative;
final description = rep.weather?.first.description;
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(roundedValue)),
color: const Color(0xFFE8F4FD),
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 90),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
weatherDTO.city ?? '',
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF6B7280)),
),
),
const SizedBox(height: 10),
// Day tabs
SizedBox(
height: 95,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: _days.length,
itemBuilder: (context, index) {
final day = _days[index];
final isSelected = index == _selectedDayIndex;
return GestureDetector(
onTap: () => setState(() => _selectedDayIndex = index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(18),
boxShadow: isSelected
? [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 2))
]
: [],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_shortDayLabel(day.dt, appContext),
style: TextStyle(
fontSize: 13,
fontWeight:
isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected
? const Color(0xFF1F2937)
: const Color(0xFF6B7280),
),
),
const SizedBox(height: 4),
CachedNetworkImage(
imageUrl:
"https://openweathermap.org/img/wn/${day.representative.weather!.first.icon!}.png",
width: 32,
height: 32,
),
Text(
'${day.maxTemp.round()}°/${day.minTemp.round()}°',
style: TextStyle(
fontSize: 11,
color: isSelected
? const Color(0xFF1F2937)
: const Color(0xFF9CA3AF),
),
),
],
),
),
);
},
),
),
const SizedBox(height: 12),
// Selected day main info
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_fullDayLabel(selected.dt, appContext),
style: const TextStyle(fontSize: 14, color: Color(0xFF6B7280)),
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'${selected.maxTemp.round()}°',
style: const TextStyle(
fontSize: 56,
fontWeight: FontWeight.w300,
color: Color(0xFF1F2937)),
),
Text(
'/${selected.minTemp.round()}°',
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.w300,
color: Color(0xFF9CA3AF)),
),
const SizedBox(width: 4),
CachedNetworkImage(
imageUrl:
"https://openweathermap.org/img/wn/${rep.weather!.first.icon!}@2x.png",
width: 64,
height: 64,
),
],
),
if (description != null && description.isNotEmpty)
Text(
description[0].toUpperCase() + description.substring(1),
style: TextStyle(
fontSize: 15,
color: primaryColor,
fontWeight: FontWeight.w500),
),
],
),
),
const SizedBox(height: 20),
// Hourly section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
TranslationHelper.getFromLocale(
"weather.hourly", appContext.getContext()),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937)),
),
),
const SizedBox(height: 8),
Container(
margin: const EdgeInsets.symmetric(horizontal: 12),
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2))
],
),
child: SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: selected.forecasts.length > 8 ? 8 : selected.forecasts.length,
itemBuilder: (context, index) {
final f = selected.forecasts[index];
final hour = DateFormat('HH:mm').format(
DateTime.fromMillisecondsSinceEpoch(f.dt! * 1000));
final pop = f.pop is num
? ((f.pop as num).toDouble() * 100).round()
: 0;
return SizedBox(
width: 72,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
'${f.main!.temp!.round()}°',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937)),
),
pop > 0
? Text(
'$pop %',
style: const TextStyle(
fontSize: 11,
color: Color(0xFF3B82F6),
fontWeight: FontWeight.w500),
)
: const SizedBox(height: 14),
CachedNetworkImage(
imageUrl:
"https://openweathermap.org/img/wn/${f.weather!.first.icon!}.png",
width: 34,
height: 34,
),
Text(
hour,
style: const TextStyle(
fontSize: 12, color: Color(0xFF6B7280)),
),
],
),
);
},
),
),
),
const SizedBox(height: 24),
],
),
),
);
}
}