diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e866b45..b716869 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,9 @@ +v2.7.7 +Improved departures/arrivals page: + - badge for platform (due to limitations in data, platform in only known when a train arrives/departs) + - time deviations shown (delays or arriving early) +Moved to API v3 for station data. + v2.7.6 Transitioned to Material 3. Redesigned main page on Material. @@ -90,4 +96,4 @@ v2.0.1 v2.0.0 Rewritten! - separate UI for Android and iOS -- uses WebView to get data on device instead of relying on server \ No newline at end of file +- uses WebView to get data on device instead of relying on server diff --git a/lib/api/station_data.dart b/lib/api/station_data.dart index 3d1ea9c..828a008 100644 --- a/lib/api/station_data.dart +++ b/lib/api/station_data.dart @@ -5,6 +5,6 @@ import 'package:info_tren/api/common.dart'; import 'package:info_tren/models/station_data.dart'; Future getStationData(String stationName) async { - final response = await http.get(Uri.https(authority, 'v2/station/$stationName')); + final response = await http.get(Uri.https(authority, 'v3/stations/$stationName')); return StationData.fromJson(jsonDecode(response.body)); -} \ No newline at end of file +} diff --git a/lib/api/stations.dart b/lib/api/stations.dart index cc51898..3f48626 100644 --- a/lib/api/stations.dart +++ b/lib/api/stations.dart @@ -8,4 +8,4 @@ Future> get stations async { final result = await http.get(Uri.https(authority, 'v2/stations')); final data = jsonDecode(result.body) as List; return data.map((e) => StationsResult.fromJson(e)).toList(growable: false,); -} \ No newline at end of file +} diff --git a/lib/models/station_data.dart b/lib/models/station_data.dart index db614dd..4dbfd71 100644 --- a/lib/models/station_data.dart +++ b/lib/models/station_data.dart @@ -6,8 +6,8 @@ part 'station_data.g.dart'; class StationData { final String date; final String stationName; - final List? arrivals; - final List? departures; + final List? arrivals; + final List? departures; const StationData({required this.date, required this.stationName, required this.arrivals, required this.departures}); @@ -16,53 +16,40 @@ class StationData { } @JsonSerializable() -class StationArrival { +class StationArrDep { final int? stoppingTime; final DateTime time; - final StationTrainArr train; + final StationTrain train; + final StationStatus status; - const StationArrival({required this.stoppingTime, required this.time, required this.train,}); + const StationArrDep({required this.stoppingTime, required this.time, required this.train, required this.status,}); - factory StationArrival.fromJson(Map json) => _$StationArrivalFromJson(json); - Map toJson() => _$StationArrivalToJson(this); + factory StationArrDep.fromJson(Map json) => _$StationArrDepFromJson(json); + Map toJson() => _$StationArrDepToJson(this); } @JsonSerializable() -class StationDeparture { - final int? stoppingTime; - final DateTime time; - final StationTrainDep train; - - const StationDeparture({required this.stoppingTime, required this.time, required this.train,}); - - factory StationDeparture.fromJson(Map json) => _$StationDepartureFromJson(json); - Map toJson() => _$StationDepartureToJson(this); -} - -@JsonSerializable() -class StationTrainArr { +class StationTrain { final String rank; final String number; final String operator; - final String origin; + final String terminus; final List? route; - StationTrainArr({required this.rank, required this.number, required this.operator, required this.origin, this.route,}); + StationTrain({required this.rank, required this.number, required this.operator, required this.terminus, this.route,}); - factory StationTrainArr.fromJson(Map json) => _$StationTrainArrFromJson(json); - Map toJson() => _$StationTrainArrToJson(this); + factory StationTrain.fromJson(Map json) => _$StationTrainFromJson(json); + Map toJson() => _$StationTrainToJson(this); } @JsonSerializable() -class StationTrainDep { - final String rank; - final String number; - final String operator; - final String destination; - final List? route; +class StationStatus { + final int delay; + final bool real; + final String? platform; - StationTrainDep({required this.rank, required this.number, required this.operator, required this.destination, this.route,}); + StationStatus({required this.delay, required this.real, required this.platform}); - factory StationTrainDep.fromJson(Map json) => _$StationTrainDepFromJson(json); - Map toJson() => _$StationTrainDepToJson(this); + factory StationStatus.fromJson(Map json) => _$StationStatusFromJson(json); + Map toJson() => _$StationStatusToJson(this); } diff --git a/lib/models/station_data.g.dart b/lib/models/station_data.g.dart index d2f7964..4953334 100644 --- a/lib/models/station_data.g.dart +++ b/lib/models/station_data.g.dart @@ -10,10 +10,10 @@ StationData _$StationDataFromJson(Map json) => StationData( date: json['date'] as String, stationName: json['stationName'] as String, arrivals: (json['arrivals'] as List?) - ?.map((e) => StationArrival.fromJson(e as Map)) + ?.map((e) => StationArrDep.fromJson(e as Map)) .toList(), departures: (json['departures'] as List?) - ?.map((e) => StationDeparture.fromJson(e as Map)) + ?.map((e) => StationArrDep.fromJson(e as Map)) .toList(), ); @@ -25,68 +25,50 @@ Map _$StationDataToJson(StationData instance) => 'departures': instance.departures, }; -StationArrival _$StationArrivalFromJson(Map json) => - StationArrival( +StationArrDep _$StationArrDepFromJson(Map json) => + StationArrDep( stoppingTime: json['stoppingTime'] as int?, time: DateTime.parse(json['time'] as String), - train: StationTrainArr.fromJson(json['train'] as Map), + train: StationTrain.fromJson(json['train'] as Map), + status: StationStatus.fromJson(json['status'] as Map), ); -Map _$StationArrivalToJson(StationArrival instance) => +Map _$StationArrDepToJson(StationArrDep instance) => { 'stoppingTime': instance.stoppingTime, 'time': instance.time.toIso8601String(), 'train': instance.train, + 'status': instance.status, }; -StationDeparture _$StationDepartureFromJson(Map json) => - StationDeparture( - stoppingTime: json['stoppingTime'] as int?, - time: DateTime.parse(json['time'] as String), - train: StationTrainDep.fromJson(json['train'] as Map), - ); - -Map _$StationDepartureToJson(StationDeparture instance) => - { - 'stoppingTime': instance.stoppingTime, - 'time': instance.time.toIso8601String(), - 'train': instance.train, - }; - -StationTrainArr _$StationTrainArrFromJson(Map json) => - StationTrainArr( +StationTrain _$StationTrainFromJson(Map json) => StationTrain( rank: json['rank'] as String, number: json['number'] as String, operator: json['operator'] as String, - origin: json['origin'] as String, + terminus: json['terminus'] as String, route: (json['route'] as List?)?.map((e) => e as String).toList(), ); -Map _$StationTrainArrToJson(StationTrainArr instance) => +Map _$StationTrainToJson(StationTrain instance) => { 'rank': instance.rank, 'number': instance.number, 'operator': instance.operator, - 'origin': instance.origin, + 'terminus': instance.terminus, 'route': instance.route, }; -StationTrainDep _$StationTrainDepFromJson(Map json) => - StationTrainDep( - rank: json['rank'] as String, - number: json['number'] as String, - operator: json['operator'] as String, - destination: json['destination'] as String, - route: - (json['route'] as List?)?.map((e) => e as String).toList(), +StationStatus _$StationStatusFromJson(Map json) => + StationStatus( + delay: json['delay'] as int, + real: json['real'] as bool, + platform: json['platform'] as String?, ); -Map _$StationTrainDepToJson(StationTrainDep instance) => +Map _$StationStatusToJson(StationStatus instance) => { - 'rank': instance.rank, - 'number': instance.number, - 'operator': instance.operator, - 'destination': instance.destination, - 'route': instance.route, + 'delay': instance.delay, + 'real': instance.real, + 'platform': instance.platform, }; diff --git a/lib/pages/station_arrdep_page/view_station/view_station.dart b/lib/pages/station_arrdep_page/view_station/view_station.dart index a8b2156..910d81a 100644 --- a/lib/pages/station_arrdep_page/view_station/view_station.dart +++ b/lib/pages/station_arrdep_page/view_station/view_station.dart @@ -30,7 +30,7 @@ class ViewStationPage extends StatefulWidget { abstract class ViewStationPageState extends State { static const arrivals = 'Sosiri'; - static const departures = 'Pleacări'; + static const departures = 'Plecări'; static const loadingText = 'Se încarcă...'; static const arrivesFrom = 'Sosește de la'; static const arrivedFrom = 'A sosit de la'; @@ -63,8 +63,8 @@ abstract class ViewStationPageState extends State { } Widget buildContent(BuildContext context, Future Function() refresh, Future Function(Future Function() newFutureBuilder) _, RefreshFutureBuilderSnapshot snapshot); - Widget buildStationArrivalItem(BuildContext context, StationArrival item); - Widget buildStationDepartureItem(BuildContext context, StationDeparture item); + Widget buildStationArrivalItem(BuildContext context, StationArrDep item); + Widget buildStationDepartureItem(BuildContext context, StationArrDep item); void onTabChange(int index) { setState(() { @@ -88,4 +88,4 @@ abstract class ViewStationPageState extends State { enum ViewStationPageTab { arrivals, departures, -} \ No newline at end of file +} diff --git a/lib/pages/station_arrdep_page/view_station/view_station_cupertino.dart b/lib/pages/station_arrdep_page/view_station/view_station_cupertino.dart index 686045f..2f0d307 100644 --- a/lib/pages/station_arrdep_page/view_station/view_station_cupertino.dart +++ b/lib/pages/station_arrdep_page/view_station/view_station_cupertino.dart @@ -1,7 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:info_tren/components/loading/loading.dart'; import 'package:info_tren/components/refresh_future_builder.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:info_tren/components/sliver_persistent_header_padding.dart'; import 'package:info_tren/models/station_data.dart'; import 'package:info_tren/pages/station_arrdep_page/view_station/view_station.dart'; @@ -53,7 +52,7 @@ class ViewStationPageStateCupertino extends ViewStationPageState { } @override - Widget buildStationArrivalItem(BuildContext context, StationArrival item) { + Widget buildStationArrivalItem(BuildContext context, StationArrDep item) { return GestureDetector( onTap: () => onTrainTapped(item.train.number), child: CupertinoFormRow( @@ -77,7 +76,7 @@ class ViewStationPageStateCupertino extends ViewStationPageState { children: [ TextSpan(text: ViewStationPageState.arrivesFrom), TextSpan(text: ' '), - TextSpan(text: item.train.origin), + TextSpan(text: item.train.terminus), ], ), ), @@ -86,7 +85,7 @@ class ViewStationPageStateCupertino extends ViewStationPageState { } @override - Widget buildStationDepartureItem(BuildContext context, StationDeparture item) { + Widget buildStationDepartureItem(BuildContext context, StationArrDep item) { return GestureDetector( onTap: () => onTrainTapped(item.train.number), child: CupertinoFormRow( @@ -110,11 +109,11 @@ class ViewStationPageStateCupertino extends ViewStationPageState { children: [ TextSpan(text: ViewStationPageState.departsTo), TextSpan(text: ' '), - TextSpan(text: item.train.destination), + TextSpan(text: item.train.terminus), ], ), ), ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/station_arrdep_page/view_station/view_station_material.dart b/lib/pages/station_arrdep_page/view_station/view_station_material.dart index cf699eb..426559e 100644 --- a/lib/pages/station_arrdep_page/view_station/view_station_material.dart +++ b/lib/pages/station_arrdep_page/view_station/view_station_material.dart @@ -1,7 +1,9 @@ +import 'dart:math'; +import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:info_tren/components/badge.dart'; import 'package:info_tren/components/loading/loading.dart'; import 'package:info_tren/components/refresh_future_builder.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:info_tren/models/station_data.dart'; import 'package:info_tren/pages/station_arrdep_page/view_station/view_station.dart'; @@ -48,64 +50,240 @@ class ViewStationPageStateMaterial extends ViewStationPageState { } @override - Widget buildStationArrivalItem(BuildContext context, StationArrival item) { - return ListTile( - leading: Text('${item.time.toLocal().hour.toString().padLeft(2, '0')}:${item.time.toLocal().minute.toString().padLeft(2, '0')}'), - title: Text.rich( - TextSpan( - children: [ - TextSpan( - text: item.train.rank, - style: TextStyle( - color: item.train.rank.startsWith('IR') ? Color.fromARGB(255, 255, 0, 0) : null, + Widget buildStationArrivalItem(BuildContext context, StationArrDep item) { + return InkWell( + onTap: () => onTrainTapped(item.train.number), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '${item.time.toLocal().hour.toString().padLeft(2, '0')}:${item.time.toLocal().minute.toString().padLeft(2, '0')}', + style: TextStyle( + inherit: true, + fontFeatures: [ + FontFeature.tabularFigures(), + ], + decoration: item.status.delay != 0 ? TextDecoration.lineThrough : null, + fontSize: item.status.delay != 0 ? 12 : null, + ), + ), + if (item.status.delay != 0) Builder( + builder: (context) { + final newTime = item.time.add(Duration(minutes: item.status.delay)); + final delay = item.status.delay > 0; + + return Text( + '${newTime.toLocal().hour.toString().padLeft(2, '0')}:${newTime.toLocal().minute.toString().padLeft(2, '0')}', + style: TextStyle( + inherit: true, + fontFeatures: [ + FontFeature.tabularFigures(), + ], + color: delay ? Colors.red : Colors.green, + ), + ); + }, + ), + ], + ), + ), + Expanded( + child: IgnorePointer( + child: ListTile( + isThreeLine: item.status.delay != 0, + title: Text.rich( + TextSpan( + children: [ + TextSpan( + text: item.train.rank, + style: TextStyle( + color: item.train.rank.startsWith('IR') ? Color.fromARGB(255, 255, 0, 0) : null, + ), + ), + TextSpan(text: ' '), + TextSpan(text: item.train.number,), + ], + ), + ), + subtitle: Text.rich( + TextSpan( + children: [ + TextSpan(text: item.time.add(Duration(minutes: max(0, item.status.delay))).compareTo(DateTime.now()) < 0 ? ViewStationPageState.arrivedFrom : ViewStationPageState.arrivesFrom), + TextSpan(text: ' '), + TextSpan(text: item.train.terminus), + if (item.status.delay != 0) ...[ + TextSpan(text: '\n'), + if (item.status.delay.abs() >= 60) ...[ + TextSpan(text: (item.status.delay.abs() ~/ 60).toString()), + TextSpan(text: item.status.delay.abs() >= 120 ? ' ore' : ' oră'), + if (item.status.delay.abs() % 60 != 0) + TextSpan(text: ' și '), + ], + TextSpan(text: (item.status.delay.abs() % 60).toString()), + TextSpan(text: item.status.delay.abs() > 1 ? ' minute' : ' minut'), + TextSpan(text: ' '), + if (item.status.delay > 0) + TextSpan( + text: 'întârziere', + style: TextStyle( + inherit: true, + color: Colors.red, + ), + ) + else + TextSpan( + text: 'mai devreme', + style: TextStyle( + inherit: true, + color: Colors.green, + ), + ), + ], + ], + ), + ), ), ), - TextSpan(text: ' '), - TextSpan(text: item.train.number,), - ], - ), - ), - subtitle: Text.rich( - TextSpan( - children: [ - TextSpan(text: item.time.compareTo(DateTime.now()) < 0 ? ViewStationPageState.arrivedFrom : ViewStationPageState.arrivesFrom), - TextSpan(text: ' '), - TextSpan(text: item.train.origin), - ], - ), + ), + if (item.status.platform != null) + IntrinsicHeight( + child: AspectRatio( + aspectRatio: 1, + child: MaterialBadge( + text: item.status.platform!, + caption: 'Linia', + isOnTime: item.status.real && item.status.delay <= 0, + isDelayed: item.status.real && item.status.delay > 0, + ), + ), + ), + ], ), - onTap: () => onTrainTapped(item.train.number), ); } @override - Widget buildStationDepartureItem(BuildContext context, StationDeparture item) { - return ListTile( - leading: Text('${item.time.toLocal().hour.toString().padLeft(2, '0')}:${item.time.toLocal().minute.toString().padLeft(2, '0')}'), - title: Text.rich( - TextSpan( - children: [ - TextSpan( - text: item.train.rank, - style: TextStyle( - color: item.train.rank.startsWith('IR') ? Color.fromARGB(255, 255, 0, 0) : null, + Widget buildStationDepartureItem(BuildContext context, StationArrDep item) { + return InkWell( + onTap: () => onTrainTapped(item.train.number), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '${item.time.toLocal().hour.toString().padLeft(2, '0')}:${item.time.toLocal().minute.toString().padLeft(2, '0')}', + style: TextStyle( + inherit: true, + fontFeatures: [ + FontFeature.tabularFigures(), + ], + decoration: item.status.delay != 0 ? TextDecoration.lineThrough : null, + fontSize: item.status.delay != 0 ? 12 : null, + ), + ), + if (item.status.delay != 0) Builder( + builder: (context) { + final newTime = item.time.add(Duration(minutes: item.status.delay)); + final delay = item.status.delay > 0; + + return Text( + '${newTime.toLocal().hour.toString().padLeft(2, '0')}:${newTime.toLocal().minute.toString().padLeft(2, '0')}', + style: TextStyle( + inherit: true, + fontFeatures: [ + FontFeature.tabularFigures(), + ], + color: delay ? Colors.red : Colors.green, + ), + ); + }, + ), + ], + ), + ), + Expanded( + child: IgnorePointer( + child: ListTile( + isThreeLine: item.status.delay != 0, + title: Text.rich( + TextSpan( + children: [ + TextSpan( + text: item.train.rank, + style: TextStyle( + color: item.train.rank.startsWith('IR') ? Color.fromARGB(255, 255, 0, 0) : null, + ), + ), + TextSpan(text: ' '), + TextSpan(text: item.train.number,), + ], + ), + ), + subtitle: Text.rich( + TextSpan( + children: [ + TextSpan(text: item.time.add(Duration(minutes: max(0, item.status.delay))).compareTo(DateTime.now()) < 0 ? ViewStationPageState.departedTo : ViewStationPageState.departsTo), + TextSpan(text: ' '), + TextSpan(text: item.train.terminus), + if (item.status.delay != 0) ...[ + TextSpan(text: '\n'), + if (item.status.delay.abs() >= 60) ...[ + TextSpan(text: (item.status.delay.abs() ~/ 60).toString()), + TextSpan(text: item.status.delay.abs() >= 120 ? ' ore' : ' oră'), + if (item.status.delay.abs() % 60 != 0) + TextSpan(text: ' și '), + ], + TextSpan(text: (item.status.delay.abs() % 60).toString()), + TextSpan(text: item.status.delay.abs() > 1 ? ' minute' : ' minut'), + TextSpan(text: ' '), + if (item.status.delay > 0) + TextSpan( + text: 'întârziere', + style: TextStyle( + inherit: true, + color: Colors.red, + ), + ) + else + TextSpan( + text: 'mai devreme', + style: TextStyle( + inherit: true, + color: Colors.green, + ), + ), + ], + ], + ), + ), ), ), - TextSpan(text: ' '), - TextSpan(text: item.train.number,), - ], - ), - ), - subtitle: Text.rich( - TextSpan( - children: [ - TextSpan(text: item.time.compareTo(DateTime.now()) < 0 ? ViewStationPageState.departedTo : ViewStationPageState.departsTo), - TextSpan(text: ' '), - TextSpan(text: item.train.destination), - ], - ), + ), + if (item.status.platform != null) + IntrinsicHeight( + child: AspectRatio( + aspectRatio: 1, + child: MaterialBadge( + text: item.status.platform!, + caption: 'Linia', + isOnTime: item.status.real && item.status.delay <= 0, + isDelayed: item.status.real && item.status.delay > 0, + ), + ), + ), + ], ), - onTap: () => onTrainTapped(item.train.number), ); } -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index 33cf28f..186120b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: O aplicație de vizualizare a datelor puse la dispoziție de Inform # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.7.6 +version: 2.7.7 environment: sdk: ">=2.15.0 <3.0.0"