From 2729f02c8ee9a0ee87164d47ada11fb87fd59ad7 Mon Sep 17 00:00:00 2001 From: Valentin REVERSAT Date: Mon, 11 Mar 2024 21:50:14 +0100 Subject: [PATCH] WIP --- lib/bloc/forecast/forecast_bloc.dart | 220 +++++++++--------- lib/bloc/forecast/forecast_state.dart | 61 +++-- lib/bloc/status/status_bloc.dart | 2 +- lib/extensions/color_scheme_extension.dart | 4 + lib/models/abstract_forecast.dart | 56 ++--- lib/models/boat_forecast.dart | 78 +++---- lib/models/enums/forecast_closing_type.dart | 4 +- lib/models/maintenance_forecast.dart | 55 ++--- lib/models/special_event_forecast.dart | 137 +++++++++++ lib/screens/error_screen.dart | 2 +- lib/screens/forecast_screen.dart | 11 +- .../forecast/forecast_list_widget.dart | 9 +- .../forecast_widget/duration_widget.dart | 2 +- .../forecast_widget/leading_icon_widget.dart | 2 +- test/units/boat_forecast_test.dart | 11 +- test/units/maintenance_forecast_test.dart | 11 +- 16 files changed, 385 insertions(+), 280 deletions(-) create mode 100644 lib/models/special_event_forecast.dart diff --git a/lib/bloc/forecast/forecast_bloc.dart b/lib/bloc/forecast/forecast_bloc.dart index f1f40a54..37f5def8 100644 --- a/lib/bloc/forecast/forecast_bloc.dart +++ b/lib/bloc/forecast/forecast_bloc.dart @@ -6,6 +6,7 @@ import 'package:chabo_app/const.dart'; import 'package:chabo_app/models/abstract_forecast.dart'; import 'package:chabo_app/models/boat_forecast.dart'; import 'package:chabo_app/models/maintenance_forecast.dart'; +import 'package:chabo_app/models/special_event_forecast.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -17,112 +18,134 @@ class ForecastBloc extends Bloc { final SentryHttpClient httpClient; ForecastBloc({required this.httpClient}) : super(const ForecastState()) { - Timer.periodic(const Duration(seconds: 1), _onRefreshCurrentStatus); + Timer.periodic(const Duration(seconds: 1), _onAutomaticTimerRefresh); on( _onForecastFetched, ); } - void _onRefreshCurrentStatus(Timer timer) { + void _onAutomaticTimerRefresh(Timer timer) async { try { - if (state.status == ForecastStatus.success) { - final currentStatus = _getCurrentStatus(state.forecasts); - final previousStatus = - _getPreviousStatus(state.forecasts, currentStatus); - if (currentStatus != state.currentForecast && - currentStatus != previousStatus) { - // ignore: invalid_use_of_visible_for_testing_member - emit( - state.copyWith( - currentForecast: currentStatus, - previousForecast: previousStatus, - ), - ); + var bridgeStatus = BridgeStatus.bridgeClosed; + var forecastStatus = ForecastStatus.forecastsFetched; + var currentForecast = state.currentForecast; + var nextForecast = state.nextForecast; + var forecasts = state.forecasts; + if (currentForecast == null) { + bridgeStatus = BridgeStatus.bridgeOpen; + if (nextForecast == null) { + forecastStatus = ForecastStatus.forecastsYetToBeAnnounced; + } else { + if (nextForecast.circulationClosingDate.isBefore(DateTime.now())) { + bridgeStatus = BridgeStatus.bridgeOpen; + currentForecast = nextForecast; + nextForecast = await _fetchNextForecast(); + forecasts = await _fetchForecasts(); + } + } + } else { + if (currentForecast.circulationReOpeningDate.isAfter(DateTime.now())) { + bridgeStatus = BridgeStatus.bridgeOpen; + currentForecast = null; } } + // ignore: invalid_use_of_visible_for_testing_member + emit(state.copyWith( + bridgeStatus: bridgeStatus, + forecastStatus: forecastStatus, + currentForecast: currentForecast, + nextForecast: nextForecast, + forecasts: forecasts, + errorMessage: DateTime.now().toIso8601String())); } catch (_) { // ignore: invalid_use_of_visible_for_testing_member emit(state.copyWith( - status: ForecastStatus.failure, - message: _.toString(), + forecastStatus: ForecastStatus.error, + errorMessage: _.toString(), )); } } - Future> _fetchForecasts( - int offset, - ) async { + Future> _fetchForecasts() async { var uri = Uri.https( - 'opendata.bordeaux-metropole.fr', - '/api/records/1.0/search', + 'beta.chabo-api.vareversat.fr', + '/v1/forecasts', { - 'dataset': 'previsions_pont_chaban', - 'rows': '${Const.forecastLimit}', - 'sort': '-date_passage', - 'start': '$offset', - 'timezone': 'Europe/Paris', + 'limit': '10', + 'offset': '0', + 'from': DateTime.now().toUtc().toIso8601String() }, ); - final response = await httpClient.get(uri); + final response = await httpClient.get(uri, headers: { + 'Timezone': 'UTC', + 'User-Agent': 'https://github.com/vareversat/chabo-app' + }); if (response.statusCode == 200) { final body = json.decode(response.body); - return (body['records'] as List).map((json) { - if (json['fields']['bateau'].toString().toLowerCase() == + return (body['forecasts'] as List).map((forecast) { + if (forecast['closing_reason'].toString().toLowerCase() == 'maintenance') { - final maintenanceForecast = MaintenanceForecast.fromJSON(json); - - return maintenanceForecast; + return MaintenanceForecast.fromJSON(forecast); + } else if (forecast['closing_reason'].toString().toLowerCase() == + 'special_event') { + return SpecialEventForecast.fromJSON(forecast); + } else { + return BoatForecast.fromJSON(forecast); } - final boatForecast = BoatForecast.fromJSON(json); - - return boatForecast; - }).toList() - ..sort((a, b) => - a.circulationClosingDate.compareTo(b.circulationClosingDate)); + }).toList(); } return []; } - AbstractForecast? _getCurrentStatus( - List forecast, - ) { - int middle = forecast.length ~/ 2; - if ((forecast[middle].circulationClosingDate.isBefore(DateTime.now()) && - forecast[middle].circulationReOpeningDate.isAfter(DateTime.now()))) { - return forecast[middle]; - } - if (forecast.length == 2) { - if (forecast[1].circulationClosingDate.isAfter(DateTime.now()) && - forecast[0].circulationReOpeningDate.isBefore(DateTime.now())) { - return forecast[1]; + Future _fetchCurrentForecast() async { + var uri = + Uri.https('beta.chabo-api.vareversat.fr', '/v1/forecasts/current'); + final response = await httpClient.get(uri, headers: { + 'Timezone': 'UTC', + 'User-Agent': 'https://github.com/vareversat/chabo-app' + }); + if (response.statusCode == 200) { + final body = json.decode(response.body); + + var forecast = body['forecast']; + + if (forecast['closing_reason'].toString().toLowerCase() == + 'maintenance') { + return MaintenanceForecast.fromJSON(forecast); + } else if (forecast['closing_reason'].toString().toLowerCase() == + 'special_event') { + return SpecialEventForecast.fromJSON(forecast); } else { - if (!forecast[0].circulationReOpeningDate.isBefore(DateTime.now())) { - return forecast[0]; - } else { - return null; - } + return BoatForecast.fromJSON(forecast); } - } else if (forecast[middle] - .circulationClosingDate - .isAfter(DateTime.now())) { - return _getCurrentStatus(forecast.sublist(0, middle + 1)); - } else { - return _getCurrentStatus(forecast.sublist(middle)); } + return null; } - AbstractForecast? _getPreviousStatus( - List forecasts, - AbstractForecast? currentStatus, - ) { - if (currentStatus == null) { - return null; + Future _fetchNextForecast() async { + var uri = Uri.https('beta.chabo-api.vareversat.fr', '/v1/forecasts/next'); + final response = await httpClient.get(uri, headers: { + 'Timezone': 'UTC', + 'User-Agent': 'https://github.com/vareversat/chabo-app' + }); + if (response.statusCode == 200) { + final body = json.decode(response.body); + + var forecast = body['forecast']; + + if (forecast['closing_reason'].toString().toLowerCase() == + 'maintenance') { + return MaintenanceForecast.fromJSON(forecast); + } else if (forecast['closing_reason'].toString().toLowerCase() == + 'special_event') { + return SpecialEventForecast.fromJSON(forecast); + } else { + return BoatForecast.fromJSON(forecast); + } } - return forecasts.indexOf(currentStatus) == 0 - ? null - : forecasts.elementAt(forecasts.indexOf(currentStatus) - 1); + return null; } Future _onForecastFetched( @@ -131,42 +154,29 @@ class ForecastBloc extends Bloc { ) async { if (state.hasReachedMax) return; try { - if (state.status == ForecastStatus.initial) { - final forecasts = await _fetchForecasts(state.offset); - final currentStatus = _getCurrentStatus(forecasts); - final noMoreForecasts = currentStatus == null; - emit(state.copyWith( - status: ForecastStatus.success, - forecasts: forecasts, - currentForecast: currentStatus, - previousForecast: _getPreviousStatus(forecasts, currentStatus), - noMoreForecasts: noMoreForecasts, - hasReachedMax: false, - offset: state.offset + Const.forecastLimit, - )); - } - final forecasts = await _fetchForecasts(state.forecasts.length); - emit( - forecasts.isEmpty - ? state.copyWith(hasReachedMax: true) - : state.copyWith( - currentForecast: - state.currentForecast ?? _getCurrentStatus(forecasts), - previousForecast: state.previousForecast ?? - _getPreviousStatus( - forecasts, - _getCurrentStatus(forecasts), - ), - status: ForecastStatus.success, - forecasts: List.of(state.forecasts)..addAll(forecasts), - hasReachedMax: false, - offset: state.offset + Const.forecastLimit, - ), - ); + final forecasts = await _fetchForecasts(); + final currentForecast = await _fetchCurrentForecast(); + final nextForecast = await _fetchNextForecast(); + final firstForecast = forecasts[0]; + + emit(state.copyWith( + bridgeStatus: currentForecast == null + ? BridgeStatus.bridgeOpen + : BridgeStatus.bridgeClosed, + forecastStatus: nextForecast == null + ? ForecastStatus.forecastsYetToBeAnnounced + : ForecastStatus.forecastsFetched, + forecasts: forecasts, + firstForecast: firstForecast, + currentForecast: currentForecast, + nextForecast: nextForecast, + hasReachedMax: false, + offset: state.offset + Const.forecastLimit, + )); } catch (_) { emit(state.copyWith( - status: ForecastStatus.failure, - message: _.toString(), + forecastStatus: ForecastStatus.error, + errorMessage: _.toString(), )); } } diff --git a/lib/bloc/forecast/forecast_state.dart b/lib/bloc/forecast/forecast_state.dart index dfa05d46..32d8cb0e 100644 --- a/lib/bloc/forecast/forecast_state.dart +++ b/lib/bloc/forecast/forecast_state.dart @@ -1,58 +1,77 @@ part of 'forecast_bloc.dart'; class ForecastState extends Equatable { - final ForecastStatus status; + final ForecastStatus forecastStatus; + final BridgeStatus bridgeStatus; final List forecasts; + final AbstractForecast? firstForecast; final AbstractForecast? currentForecast; - final AbstractForecast? previousForecast; - final bool noMoreForecasts; + final AbstractForecast? nextForecast; final bool hasReachedMax; final int offset; - final String message; + final String errorMessage; const ForecastState({ - this.status = ForecastStatus.initial, + this.forecastStatus = ForecastStatus.toBeFetched, + this.bridgeStatus = BridgeStatus.toBeDetermined, this.forecasts = const [], + this.firstForecast, this.currentForecast, - this.previousForecast, this.hasReachedMax = false, this.offset = 0, - this.message = 'OK', - this.noMoreForecasts = false, + this.errorMessage = 'OK', + this.nextForecast, }); ForecastState copyWith({ - ForecastStatus? status, + ForecastStatus? forecastStatus, + BridgeStatus? bridgeStatus, List? forecasts, + AbstractForecast? firstForecast, AbstractForecast? currentForecast, - AbstractForecast? previousForecast, + AbstractForecast? nextForecast, bool? noMoreForecasts, bool? hasReachedMax, int? offset, - String? message, + String? errorMessage, }) { return ForecastState( - status: status ?? this.status, + forecastStatus: forecastStatus ?? this.forecastStatus, + bridgeStatus: bridgeStatus ?? this.bridgeStatus, forecasts: forecasts ?? this.forecasts, + firstForecast: firstForecast ?? this.firstForecast, currentForecast: currentForecast ?? this.currentForecast, - previousForecast: previousForecast ?? this.previousForecast, - noMoreForecasts: noMoreForecasts ?? this.noMoreForecasts, + nextForecast: nextForecast ?? this.nextForecast, hasReachedMax: hasReachedMax ?? this.hasReachedMax, offset: offset ?? this.offset, - message: message ?? this.message, + errorMessage: errorMessage ?? this.errorMessage, ); } @override - List get props => [ - status, + List get props => + [ + forecastStatus, + bridgeStatus, forecasts, + firstForecast, + currentForecast, + nextForecast, hasReachedMax, offset, - message, - currentForecast, - previousForecast, + errorMessage, ]; } -enum ForecastStatus { initial, success, failure } +enum ForecastStatus { + toBeFetched, + forecastsFetched, + forecastsYetToBeAnnounced, + error +} + +enum BridgeStatus { + toBeDetermined, + bridgeOpen, + bridgeClosed, +} diff --git a/lib/bloc/status/status_bloc.dart b/lib/bloc/status/status_bloc.dart index 6e44b1d1..765d0715 100644 --- a/lib/bloc/status/status_bloc.dart +++ b/lib/bloc/status/status_bloc.dart @@ -184,7 +184,7 @@ class StatusBloc extends Bloc { final previousForecast = state.previousForecast; if (currentForecast != null && previousForecast != null) { return currentForecast.isCurrentlyClosed() - ? currentForecast.closedDuration + ? currentForecast.closingDuration : currentForecast.circulationClosingDate.difference( previousForecast.circulationReOpeningDate, ); diff --git a/lib/extensions/color_scheme_extension.dart b/lib/extensions/color_scheme_extension.dart index 12b22df7..834c4222 100644 --- a/lib/extensions/color_scheme_extension.dart +++ b/lib/extensions/color_scheme_extension.dart @@ -17,6 +17,10 @@ extension ColorSchemeExtension on ColorScheme { return brightness == Brightness.light ? Colors.brown : Colors.grey; } + MaterialColor get specialEventColor { + return brightness == Brightness.light ? Colors.purple : Colors.pink; + } + Color get okColor { return brightness == Brightness.light ? const Color(0xFF81C784) diff --git a/lib/models/abstract_forecast.dart b/lib/models/abstract_forecast.dart index c4e5c258..d063c72e 100644 --- a/lib/models/abstract_forecast.dart +++ b/lib/models/abstract_forecast.dart @@ -16,50 +16,25 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; abstract class AbstractForecast extends Equatable { - final bool totalClosing; - late final bool isDuringTwoDays; + final String id; final ForecastClosingReason closingReason; - late final Duration closedDuration; - late final DateTime _circulationClosingDate; - late final DateTime _circulationReOpeningDate; + final Duration closingDuration; + final DateTime circulationClosingDate; + final DateTime circulationReOpeningDate; final ForecastClosingType closingType; final List interferingTimeSlots = []; + late final bool isDuringTwoDays; AbstractForecast({ - required this.totalClosing, required this.closingReason, - required DateTime circulationClosingDate, - required DateTime circulationReOpeningDate, + required this.circulationClosingDate, + required this.circulationReOpeningDate, required this.closingType, + required this.closingDuration, + required this.id, }) { - _circulationClosingDate = circulationClosingDate.toLocal(); - - var tmpCirculationReOpeningDate = circulationReOpeningDate.toLocal(); - var tmpDuration = - tmpCirculationReOpeningDate.difference(_circulationClosingDate); - - if (tmpDuration.isNegative) { - tmpCirculationReOpeningDate = - tmpCirculationReOpeningDate.add(const Duration(days: 1)); - tmpDuration = - tmpCirculationReOpeningDate.difference(_circulationClosingDate); - } isDuringTwoDays = - tmpCirculationReOpeningDate.day != _circulationClosingDate.day; - _circulationReOpeningDate = tmpCirculationReOpeningDate; - closedDuration = tmpDuration; - } - - DateTime get circulationReOpeningDate => _circulationReOpeningDate.toLocal(); - - set circulationReOpeningDate(DateTime value) { - _circulationReOpeningDate = value; - } - - DateTime get circulationClosingDate => _circulationClosingDate.toLocal(); - - set circulationClosingDate(DateTime value) { - _circulationClosingDate = value; + circulationClosingDate.day != circulationReOpeningDate.day; } RichText getInformationWidget(BuildContext context); @@ -170,7 +145,7 @@ abstract class AbstractForecast extends Equatable { String getNotificationClosingMessage(BuildContext context); - String getClosingReason(BuildContext context); + String getLeadingIconText(BuildContext context); Color getColor(BuildContext context, bool reversed); @@ -276,10 +251,9 @@ abstract class AbstractForecast extends Equatable { Map toJson() { return { - 'total_closing': totalClosing, 'is_during_dwo_days': isDuringTwoDays, 'closing_reason': closingReason.name, - 'closed_duration': closedDuration.toString(), + 'closed_duration': closingDuration.toString(), 'closing_type': closingType.name, 'circulation_closing_date': circulationClosingDate, 'circulation_re_opening_date': circulationReOpeningDate, @@ -287,10 +261,10 @@ abstract class AbstractForecast extends Equatable { } @override - List get props => [ - totalClosing, + List get props => + [ closingReason, - closedDuration, + closingDuration, circulationClosingDate, circulationReOpeningDate, closingType, diff --git a/lib/models/boat_forecast.dart b/lib/models/boat_forecast.dart index 6da04cd7..5aa12864 100644 --- a/lib/models/boat_forecast.dart +++ b/lib/models/boat_forecast.dart @@ -8,6 +8,7 @@ import 'package:chabo_app/models/boat.dart'; import 'package:chabo_app/models/enums/forecast_closing_reason.dart'; import 'package:chabo_app/models/enums/forecast_closing_type.dart'; import 'package:chabo_app/models/enums/time_format.dart'; +import 'package:enum_to_string/enum_to_string.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -19,9 +20,10 @@ class BoatForecast extends AbstractForecast { static final List allBoatNames = []; BoatForecast({ - required super.totalClosing, + required super.id, required super.circulationClosingDate, required super.circulationReOpeningDate, + required super.closingDuration, required this.boats, required super.closingType, }) : assert(boats.isNotEmpty), @@ -31,63 +33,41 @@ class BoatForecast extends AbstractForecast { factory BoatForecast.fake() { return BoatForecast( - totalClosing: true, + id: 'id', circulationClosingDate: DateTime.now(), circulationReOpeningDate: DateTime.now(), + closingDuration: const Duration(hours: 1), boats: [Boat.fake()], - closingType: ForecastClosingType.complete); + closingType: ForecastClosingType.two_way); } factory BoatForecast.fromJSON(Map json) { - var apiTimezone = AbstractForecast.getApiTimeZone(json['record_timestamp']); - var closingDate = AbstractForecast.parseFieldDate( - json, - 'fermeture_a_la_circulation', - apiTimezone, - ); - var reopeningDate = AbstractForecast.parseFieldDate( - json, - 're_ouverture_a_la_circulation', - apiTimezone, - ); - var closingType = - (json['fields']['type_de_fermeture'] as String).toLowerCase() == - 'totale' - ? ForecastClosingType.complete - : ForecastClosingType.partial; - var totalClosing = AbstractForecast.getBooleanTotalClosingValue( - json['fields']['fermeture_totale'], - ); - List boats = []; - bool isLeaving = false; - final rawBoatName = json['fields']['bateau'] as String; - final boatNames = rawBoatName.split(RegExp(r'/')); - for (final boatName in boatNames) { - final trimmedBoatName = boatName.trim(); - isLeaving = allBoatNames.contains(trimmedBoatName); - boats.add(Boat(name: trimmedBoatName, isLeaving: isLeaving)); - if (isLeaving) { - allBoatNames.remove(trimmedBoatName); - } else { - allBoatNames.add(trimmedBoatName); - } + final jsonBoats = json['boats']; + for (final boat in jsonBoats) { + boats.add(Boat( + name: boat['name'], + isLeaving: boat['maneuver'] == 'leaving_bordeaux')); } return BoatForecast( + id: json['id'], boats: boats, - totalClosing: totalClosing, - circulationReOpeningDate: reopeningDate, - circulationClosingDate: closingDate, - closingType: closingType, + closingDuration: Duration(minutes: json['closing_duration_min'].toInt()), + circulationReOpeningDate: + DateTime.parse(json['circulation_reopening_date']), + circulationClosingDate: DateTime.parse(json['circulation_closing_date']), + closingType: EnumToString.fromString( + ForecastClosingType.values, json['closing_type']) ?? + ForecastClosingType.two_way, ); } @override - List get props => [ - totalClosing, + List get props => + [ closingReason, - closedDuration, + closingDuration, boats, circulationClosingDate, circulationReOpeningDate, @@ -100,7 +80,7 @@ class BoatForecast extends AbstractForecast { final colorScheme = Theme.of(context).colorScheme; var schedule = circulationClosingDate - .add(Duration(microseconds: closedDuration.inMicroseconds ~/ 2)); + .add(Duration(microseconds: closingDuration.inMicroseconds ~/ 2)); var scheduleString = DateFormat( timeFormat.icuName, Localizations.localeOf(context).languageCode) .format(schedule); @@ -139,7 +119,7 @@ class BoatForecast extends AbstractForecast { return AppLocalizations.of(context)!.notificationDurationBoatMessage( boats.toLocalizedString(context), pickedDuration, - closedDuration.durationToString(context), + closingDuration.durationToString(context), ); } @@ -149,7 +129,7 @@ class BoatForecast extends AbstractForecast { return AppLocalizations.of(context)!.notificationTimeBoatMessage( boats.toLocalizedString(context), DateFormat(timeFormat.icuName).format(circulationClosingDate), - closedDuration.durationToString(context), + closingDuration.durationToString(context), ); } @@ -157,7 +137,7 @@ class BoatForecast extends AbstractForecast { String getNotificationClosingMessage(BuildContext context) { return AppLocalizations.of(context)!.notificationClosingBoatMessage( boats.toLocalizedString(context), - closedDuration.durationToString(context), + closingDuration.durationToString(context), ); } @@ -232,10 +212,8 @@ class BoatForecast extends AbstractForecast { } @override - String getClosingReason(BuildContext context) { - return boats.isWineFestival() - ? AppLocalizations.of(context)!.wineFestivalSailBoats - : boats.getNames(context); + String getLeadingIconText(BuildContext context) { + return boats.getNames(context); } @override diff --git a/lib/models/enums/forecast_closing_type.dart b/lib/models/enums/forecast_closing_type.dart index 58f7e2cb..4679ea5c 100644 --- a/lib/models/enums/forecast_closing_type.dart +++ b/lib/models/enums/forecast_closing_type.dart @@ -1,4 +1,4 @@ enum ForecastClosingType { - partial, - complete; + one_way, + two_way; } diff --git a/lib/models/maintenance_forecast.dart b/lib/models/maintenance_forecast.dart index 4528da2d..d42690ea 100644 --- a/lib/models/maintenance_forecast.dart +++ b/lib/models/maintenance_forecast.dart @@ -5,6 +5,7 @@ import 'package:chabo_app/models/abstract_forecast.dart'; import 'package:chabo_app/models/enums/forecast_closing_reason.dart'; import 'package:chabo_app/models/enums/forecast_closing_type.dart'; import 'package:chabo_app/models/enums/time_format.dart'; +import 'package:enum_to_string/enum_to_string.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -12,9 +13,10 @@ import 'package:intl/intl.dart'; class MaintenanceForecast extends AbstractForecast { MaintenanceForecast({ - required super.totalClosing, + required super.id, required super.circulationClosingDate, required super.circulationReOpeningDate, + required super.closingDuration, required super.closingType, }) : super( closingReason: ForecastClosingReason.maintenance, @@ -22,46 +24,31 @@ class MaintenanceForecast extends AbstractForecast { factory MaintenanceForecast.fake() { return MaintenanceForecast( - totalClosing: true, + id: 'id', circulationClosingDate: DateTime.now(), circulationReOpeningDate: DateTime.now(), - closingType: ForecastClosingType.complete); + closingDuration: const Duration(hours: 1), + closingType: ForecastClosingType.two_way); } factory MaintenanceForecast.fromJSON(Map json) { - var apiTimezone = AbstractForecast.getApiTimeZone(json['record_timestamp']); - var closingDate = AbstractForecast.parseFieldDate( - json, - 'fermeture_a_la_circulation', - apiTimezone, - ); - var reopeningDate = AbstractForecast.parseFieldDate( - json, - 're_ouverture_a_la_circulation', - apiTimezone, - ); - var closingType = - (json['fields']['type_de_fermeture'] as String).toLowerCase() == - 'totale' - ? ForecastClosingType.complete - : ForecastClosingType.partial; - var totalClosing = AbstractForecast.getBooleanTotalClosingValue( - json['fields']['fermeture_totale'], - ); - return MaintenanceForecast( - totalClosing: totalClosing, - circulationReOpeningDate: reopeningDate, - circulationClosingDate: closingDate, - closingType: closingType, + id: json['id'], + closingDuration: Duration(minutes: json['closing_duration_min'].toInt()), + circulationReOpeningDate: + DateTime.parse(json['circulation_reopening_date']), + circulationClosingDate: DateTime.parse(json['circulation_closing_date']), + closingType: EnumToString.fromString( + ForecastClosingType.values, json['closing_type']) ?? + ForecastClosingType.two_way, ); } @override - List get props => [ - totalClosing, + List get props => + [ closingReason, - closedDuration, + closingDuration, circulationClosingDate, circulationReOpeningDate, closingType, @@ -74,7 +61,7 @@ class MaintenanceForecast extends AbstractForecast { ) { return AppLocalizations.of(context)!.notificationDurationMaintenanceMessage( pickedDuration, - closedDuration.durationToString(context), + closingDuration.durationToString(context), ); } @@ -83,14 +70,14 @@ class MaintenanceForecast extends AbstractForecast { final timeFormat = context.read().state.timeFormat; return AppLocalizations.of(context)!.notificationTimeMaintenanceMessage( DateFormat(timeFormat.icuName).format(circulationClosingDate), - closedDuration.durationToString(context), + closingDuration.durationToString(context), ); } @override String getNotificationClosingMessage(BuildContext context) { return AppLocalizations.of(context)!.notificationClosingMaintenanceMessage( - closedDuration.durationToString( + closingDuration.durationToString( context, ), ); @@ -140,7 +127,7 @@ class MaintenanceForecast extends AbstractForecast { } @override - String getClosingReason(BuildContext context) { + String getLeadingIconText(BuildContext context) { return AppLocalizations.of(context)! .dialogInformationContentBridge_closed_maintenance .toUpperCase(); diff --git a/lib/models/special_event_forecast.dart b/lib/models/special_event_forecast.dart new file mode 100644 index 00000000..b18d5659 --- /dev/null +++ b/lib/models/special_event_forecast.dart @@ -0,0 +1,137 @@ +import 'package:chabo_app/cubits/time_format_cubit.dart'; +import 'package:chabo_app/extensions/color_scheme_extension.dart'; +import 'package:chabo_app/extensions/duration_extension.dart'; +import 'package:chabo_app/models/abstract_forecast.dart'; +import 'package:chabo_app/models/enums/forecast_closing_reason.dart'; +import 'package:chabo_app/models/enums/forecast_closing_type.dart'; +import 'package:chabo_app/models/enums/time_format.dart'; +import 'package:enum_to_string/enum_to_string.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; + +class SpecialEventForecast extends AbstractForecast { + final String specialEventName; + + SpecialEventForecast({ + required this.specialEventName, + required super.id, + required super.circulationClosingDate, + required super.circulationReOpeningDate, + required super.closingDuration, + required super.closingType, + }) : super( + closingReason: ForecastClosingReason.maintenance, + ); + + factory SpecialEventForecast.fake() { + return SpecialEventForecast( + id: 'id', + specialEventName: 'special_event_name', + circulationClosingDate: DateTime.now(), + circulationReOpeningDate: DateTime.now(), + closingDuration: const Duration(hours: 1), + closingType: ForecastClosingType.two_way); + } + + factory SpecialEventForecast.fromJSON(Map json) { + return SpecialEventForecast( + id: json['id'], + specialEventName: json['special_event_name'], + closingDuration: Duration(minutes: json['closing_duration_min'].toInt()), + circulationReOpeningDate: + DateTime.parse(json['circulation_reopening_date']), + circulationClosingDate: DateTime.parse(json['circulation_closing_date']), + closingType: EnumToString.fromString( + ForecastClosingType.values, json['closing_type']) ?? + ForecastClosingType.two_way, + ); + } + + @override + List get props => [ + closingReason, + closingDuration, + circulationClosingDate, + circulationReOpeningDate, + closingType, + ]; + + @override + String getNotificationDurationMessage( + BuildContext context, + String pickedDuration, + ) { + return AppLocalizations.of(context)!.notificationDurationMaintenanceMessage( + pickedDuration, + closingDuration.durationToString(context), + ); + } + + @override + String getNotificationTimeMessage(BuildContext context) { + final timeFormat = context.read().state.timeFormat; + return AppLocalizations.of(context)!.notificationTimeMaintenanceMessage( + DateFormat(timeFormat.icuName).format(circulationClosingDate), + closingDuration.durationToString(context), + ); + } + + @override + String getNotificationClosingMessage(BuildContext context) { + return AppLocalizations.of(context)!.notificationClosingMaintenanceMessage( + closingDuration.durationToString( + context, + ), + ); + } + + @override + RichText getInformationWidget(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return RichText( + text: TextSpan( + style: Theme.of(context).textTheme.bodyLarge?.copyWith(height: 1.6), + children: [ + ...getCoreInformationWidget(context), + TextSpan( + text: AppLocalizations.of(context)! + .dialogInformationContentBridge_closed_maintenance, + style: TextStyle( + color: colorScheme.maintenanceColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } + + @override + Widget getIconWidget( + BuildContext context, + bool reversed, + double size, + bool isLight, + ) { + return Icon( + Icons.calendar_month_sharp, + color: getColor(context, reversed), + size: size * 0.9, + ); + } + + @override + Color getColor(BuildContext context, bool reversed) { + return reversed + ? Theme.of(context).dialogBackgroundColor + : Theme.of(context).colorScheme.specialEventColor; + } + + @override + String getLeadingIconText(BuildContext context) { + return specialEventName.toUpperCase(); + } +} diff --git a/lib/screens/error_screen.dart b/lib/screens/error_screen.dart index 251446ce..b286fd3e 100644 --- a/lib/screens/error_screen.dart +++ b/lib/screens/error_screen.dart @@ -24,7 +24,7 @@ class _ErrorScreenState extends CustomWidgetState { mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( - flex: 3, + flex: 2, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/screens/forecast_screen.dart b/lib/screens/forecast_screen.dart index 68015c0d..f921cb69 100644 --- a/lib/screens/forecast_screen.dart +++ b/lib/screens/forecast_screen.dart @@ -45,10 +45,11 @@ class _ForecastScreenState extends CustomWidgetState { top: false, child: BlocBuilder( builder: (context, forecastState) { - switch (forecastState.status) { - case ForecastStatus.failure: - return ErrorScreen(errorMessage: forecastState.message); - case ForecastStatus.success: + switch (forecastState.forecastStatus) { + case ForecastStatus.error: + return ErrorScreen( + errorMessage: forecastState.errorMessage); + case ForecastStatus.forecastsFetched: if (forecastState.forecasts.isEmpty) { return const ErrorScreen(errorMessage: 'Empty return'); } @@ -61,7 +62,7 @@ class _ForecastScreenState extends CustomWidgetState { BlocProvider.of(context).add( StatusChanged( currentForecast: state.currentForecast, - previousForecast: state.previousForecast, + previousForecast: state.firstForecast, ), ); diff --git a/lib/widgets/forecast/forecast_list_widget.dart b/lib/widgets/forecast/forecast_list_widget.dart index 5940df04..085bf38c 100644 --- a/lib/widgets/forecast/forecast_list_widget.dart +++ b/lib/widgets/forecast/forecast_list_widget.dart @@ -26,7 +26,8 @@ class _ForecastListWidgetState extends State { child: BlocBuilder( builder: (context, timeSlotState) { // Check if the last forecast is before today - if (forecastState.noMoreForecasts) { + if (forecastState.forecastStatus == + ForecastStatus.forecastsYetToBeAnnounced) { return const NoMoreForecastsWidget(); } return ListView.separated( @@ -45,8 +46,8 @@ class _ForecastListWidgetState extends State { return !forecast.hasPassed() ? ForecastWidget( - key: GlobalObjectKey(forecast.hashCode), - isCurrent: forecast == forecastState.currentForecast, + key: GlobalObjectKey(forecast.id), + isCurrent: forecast == forecastState.firstForecast, hasPassed: forecast.hasPassed(), forecast: forecast, index: index, @@ -65,7 +66,7 @@ class _ForecastListWidgetState extends State { .circulationClosingDate.month) && !forecast.hasPassed() || forecastState.forecasts[index + 1] == - forecastState.currentForecast) { + forecastState.firstForecast) { return _MonthWidget( forecast: forecastState.forecasts[index + 1], ); diff --git a/lib/widgets/forecast/forecast_widget/duration_widget.dart b/lib/widgets/forecast/forecast_widget/duration_widget.dart index bed7fa35..bceba19c 100644 --- a/lib/widgets/forecast/forecast_widget/duration_widget.dart +++ b/lib/widgets/forecast/forecast_widget/duration_widget.dart @@ -21,7 +21,7 @@ class _DurationWidget extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(4.0), child: Text( - forecast.closedDuration.durationToString(context), + forecast.closingDuration.durationToString(context), style: Theme.of(context).textTheme.labelSmall, textAlign: TextAlign.center, ), diff --git a/lib/widgets/forecast/forecast_widget/leading_icon_widget.dart b/lib/widgets/forecast/forecast_widget/leading_icon_widget.dart index f5c85f64..99da49bc 100644 --- a/lib/widgets/forecast/forecast_widget/leading_icon_widget.dart +++ b/lib/widgets/forecast/forecast_widget/leading_icon_widget.dart @@ -26,7 +26,7 @@ class _LeadingIconWidget extends StatelessWidget { ), Center( child: Text( - forecast.getClosingReason(context), + forecast.getLeadingIconText(context), style: Theme.of(context) .textTheme .labelSmall diff --git a/test/units/boat_forecast_test.dart b/test/units/boat_forecast_test.dart index f2ada957..d007be40 100644 --- a/test/units/boat_forecast_test.dart +++ b/test/units/boat_forecast_test.dart @@ -10,27 +10,24 @@ import '../localized_testable_widget.dart'; void main() { final forecast = BoatForecast( - totalClosing: true, circulationClosingDate: DateTime(2023, 5, 14, 15, 0), circulationReOpeningDate: DateTime(2023, 5, 14, 16, 0), boats: [Boat(name: 'TEST_BOAT', isLeaving: false)], - closingType: ForecastClosingType.complete, + closingType: ForecastClosingType.two_way, ); final forecast2 = BoatForecast( - totalClosing: true, circulationClosingDate: DateTime(2023, 5, 14, 23, 0), circulationReOpeningDate: DateTime(2023, 5, 14, 05, 0), boats: [Boat(name: 'TEST_BOAT', isLeaving: false)], - closingType: ForecastClosingType.complete, + closingType: ForecastClosingType.two_way, ); final forecast3 = BoatForecast( - totalClosing: true, circulationClosingDate: DateTime(2023, 5, 14, 23, 0), circulationReOpeningDate: DateTime(2023, 5, 15, 05, 0), boats: [Boat(name: 'TEST_BOAT', isLeaving: false)], - closingType: ForecastClosingType.complete, + closingType: ForecastClosingType.two_way, ); test('Is NOT currently closed', () { @@ -78,7 +75,7 @@ void main() { }); test('Get the correct closing duration', () { - expect(forecast.closedDuration, const Duration(hours: 1)); + expect(forecast.closingDuration, const Duration(hours: 1)); }); group('Info TextSpan', () { diff --git a/test/units/maintenance_forecast_test.dart b/test/units/maintenance_forecast_test.dart index 9cae836d..5344d186 100644 --- a/test/units/maintenance_forecast_test.dart +++ b/test/units/maintenance_forecast_test.dart @@ -9,24 +9,21 @@ import '../localized_testable_widget.dart'; void main() { final forecast = MaintenanceForecast( - totalClosing: true, circulationClosingDate: DateTime(2023, 5, 14, 15, 0), circulationReOpeningDate: DateTime(2023, 5, 14, 16, 0), - closingType: ForecastClosingType.complete, + closingType: ForecastClosingType.two_way, ); final forecast2 = MaintenanceForecast( - totalClosing: true, circulationClosingDate: DateTime(2023, 5, 14, 23, 0), circulationReOpeningDate: DateTime(2023, 5, 14, 05, 0), - closingType: ForecastClosingType.complete, + closingType: ForecastClosingType.two_way, ); final forecast3 = MaintenanceForecast( - totalClosing: true, circulationClosingDate: DateTime(2023, 5, 14, 23, 0), circulationReOpeningDate: DateTime(2023, 5, 15, 05, 0), - closingType: ForecastClosingType.complete, + closingType: ForecastClosingType.two_way, ); test('Is NOT currently closed', () { @@ -40,7 +37,7 @@ void main() { }); test('Get the correct closing duration', () { - expect(forecast.closedDuration, const Duration(hours: 1)); + expect(forecast.closingDuration, const Duration(hours: 1)); }); group('Info TextSpan', () {