390 lines
15 KiB
Dart
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),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|