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 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 createState() => _WeatherPageState(); } class _WeatherPageState extends State { 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 allForecasts) { final Map> 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(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), ], ), ), ); } }