Browse Source

Improve departures/arrivals page

Show badge for platform
Show time deviations (delays, arriving early)
Move to API v3
master v2.7.7
Kenneth Bruen 2 years ago
parent
commit
3c68cf7164
Signed by: kbruen
GPG Key ID: C1980A470C3EE5B1
  1. 6
      CHANGELOG.txt
  2. 2
      lib/api/station_data.dart
  3. 53
      lib/models/station_data.dart
  4. 60
      lib/models/station_data.g.dart
  5. 6
      lib/pages/station_arrdep_page/view_station/view_station.dart
  6. 9
      lib/pages/station_arrdep_page/view_station/view_station_cupertino.dart
  7. 204
      lib/pages/station_arrdep_page/view_station/view_station_material.dart
  8. 2
      pubspec.yaml

6
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 v2.7.6
Transitioned to Material 3. Transitioned to Material 3.
Redesigned main page on Material. Redesigned main page on Material.

2
lib/api/station_data.dart

@ -5,6 +5,6 @@ import 'package:info_tren/api/common.dart';
import 'package:info_tren/models/station_data.dart'; import 'package:info_tren/models/station_data.dart';
Future<StationData> getStationData(String stationName) async { Future<StationData> 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)); return StationData.fromJson(jsonDecode(response.body));
} }

53
lib/models/station_data.dart

@ -6,8 +6,8 @@ part 'station_data.g.dart';
class StationData { class StationData {
final String date; final String date;
final String stationName; final String stationName;
final List<StationArrival>? arrivals; final List<StationArrDep>? arrivals;
final List<StationDeparture>? departures; final List<StationArrDep>? departures;
const StationData({required this.date, required this.stationName, required this.arrivals, required this.departures}); const StationData({required this.date, required this.stationName, required this.arrivals, required this.departures});
@ -16,53 +16,40 @@ class StationData {
} }
@JsonSerializable() @JsonSerializable()
class StationArrival { class StationArrDep {
final int? stoppingTime; final int? stoppingTime;
final DateTime time; 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<String, dynamic> json) => _$StationArrivalFromJson(json); factory StationArrDep.fromJson(Map<String, dynamic> json) => _$StationArrDepFromJson(json);
Map<String, dynamic> toJson() => _$StationArrivalToJson(this); Map<String, dynamic> toJson() => _$StationArrDepToJson(this);
} }
@JsonSerializable() @JsonSerializable()
class StationDeparture { class StationTrain {
final int? stoppingTime;
final DateTime time;
final StationTrainDep train;
const StationDeparture({required this.stoppingTime, required this.time, required this.train,});
factory StationDeparture.fromJson(Map<String, dynamic> json) => _$StationDepartureFromJson(json);
Map<String, dynamic> toJson() => _$StationDepartureToJson(this);
}
@JsonSerializable()
class StationTrainArr {
final String rank; final String rank;
final String number; final String number;
final String operator; final String operator;
final String origin; final String terminus;
final List<String>? route; final List<String>? 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<String, dynamic> json) => _$StationTrainArrFromJson(json); factory StationTrain.fromJson(Map<String, dynamic> json) => _$StationTrainFromJson(json);
Map<String, dynamic> toJson() => _$StationTrainArrToJson(this); Map<String, dynamic> toJson() => _$StationTrainToJson(this);
} }
@JsonSerializable() @JsonSerializable()
class StationTrainDep { class StationStatus {
final String rank; final int delay;
final String number; final bool real;
final String operator; final String? platform;
final String destination;
final List<String>? route;
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<String, dynamic> json) => _$StationTrainDepFromJson(json); factory StationStatus.fromJson(Map<String, dynamic> json) => _$StationStatusFromJson(json);
Map<String, dynamic> toJson() => _$StationTrainDepToJson(this); Map<String, dynamic> toJson() => _$StationStatusToJson(this);
} }

60
lib/models/station_data.g.dart

@ -10,10 +10,10 @@ StationData _$StationDataFromJson(Map<String, dynamic> json) => StationData(
date: json['date'] as String, date: json['date'] as String,
stationName: json['stationName'] as String, stationName: json['stationName'] as String,
arrivals: (json['arrivals'] as List<dynamic>?) arrivals: (json['arrivals'] as List<dynamic>?)
?.map((e) => StationArrival.fromJson(e as Map<String, dynamic>)) ?.map((e) => StationArrDep.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
departures: (json['departures'] as List<dynamic>?) departures: (json['departures'] as List<dynamic>?)
?.map((e) => StationDeparture.fromJson(e as Map<String, dynamic>)) ?.map((e) => StationArrDep.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
); );
@ -25,68 +25,50 @@ Map<String, dynamic> _$StationDataToJson(StationData instance) =>
'departures': instance.departures, 'departures': instance.departures,
}; };
StationArrival _$StationArrivalFromJson(Map<String, dynamic> json) => StationArrDep _$StationArrDepFromJson(Map<String, dynamic> json) =>
StationArrival( StationArrDep(
stoppingTime: json['stoppingTime'] as int?, stoppingTime: json['stoppingTime'] as int?,
time: DateTime.parse(json['time'] as String), time: DateTime.parse(json['time'] as String),
train: StationTrainArr.fromJson(json['train'] as Map<String, dynamic>), train: StationTrain.fromJson(json['train'] as Map<String, dynamic>),
status: StationStatus.fromJson(json['status'] as Map<String, dynamic>),
); );
Map<String, dynamic> _$StationArrivalToJson(StationArrival instance) => Map<String, dynamic> _$StationArrDepToJson(StationArrDep instance) =>
<String, dynamic>{ <String, dynamic>{
'stoppingTime': instance.stoppingTime, 'stoppingTime': instance.stoppingTime,
'time': instance.time.toIso8601String(), 'time': instance.time.toIso8601String(),
'train': instance.train, 'train': instance.train,
'status': instance.status,
}; };
StationDeparture _$StationDepartureFromJson(Map<String, dynamic> json) => StationTrain _$StationTrainFromJson(Map<String, dynamic> json) => StationTrain(
StationDeparture(
stoppingTime: json['stoppingTime'] as int?,
time: DateTime.parse(json['time'] as String),
train: StationTrainDep.fromJson(json['train'] as Map<String, dynamic>),
);
Map<String, dynamic> _$StationDepartureToJson(StationDeparture instance) =>
<String, dynamic>{
'stoppingTime': instance.stoppingTime,
'time': instance.time.toIso8601String(),
'train': instance.train,
};
StationTrainArr _$StationTrainArrFromJson(Map<String, dynamic> json) =>
StationTrainArr(
rank: json['rank'] as String, rank: json['rank'] as String,
number: json['number'] as String, number: json['number'] as String,
operator: json['operator'] as String, operator: json['operator'] as String,
origin: json['origin'] as String, terminus: json['terminus'] as String,
route: route:
(json['route'] as List<dynamic>?)?.map((e) => e as String).toList(), (json['route'] as List<dynamic>?)?.map((e) => e as String).toList(),
); );
Map<String, dynamic> _$StationTrainArrToJson(StationTrainArr instance) => Map<String, dynamic> _$StationTrainToJson(StationTrain instance) =>
<String, dynamic>{ <String, dynamic>{
'rank': instance.rank, 'rank': instance.rank,
'number': instance.number, 'number': instance.number,
'operator': instance.operator, 'operator': instance.operator,
'origin': instance.origin, 'terminus': instance.terminus,
'route': instance.route, 'route': instance.route,
}; };
StationTrainDep _$StationTrainDepFromJson(Map<String, dynamic> json) => StationStatus _$StationStatusFromJson(Map<String, dynamic> json) =>
StationTrainDep( StationStatus(
rank: json['rank'] as String, delay: json['delay'] as int,
number: json['number'] as String, real: json['real'] as bool,
operator: json['operator'] as String, platform: json['platform'] as String?,
destination: json['destination'] as String,
route:
(json['route'] as List<dynamic>?)?.map((e) => e as String).toList(),
); );
Map<String, dynamic> _$StationTrainDepToJson(StationTrainDep instance) => Map<String, dynamic> _$StationStatusToJson(StationStatus instance) =>
<String, dynamic>{ <String, dynamic>{
'rank': instance.rank, 'delay': instance.delay,
'number': instance.number, 'real': instance.real,
'operator': instance.operator, 'platform': instance.platform,
'destination': instance.destination,
'route': instance.route,
}; };

6
lib/pages/station_arrdep_page/view_station/view_station.dart

@ -30,7 +30,7 @@ class ViewStationPage extends StatefulWidget {
abstract class ViewStationPageState extends State<ViewStationPage> { abstract class ViewStationPageState extends State<ViewStationPage> {
static const arrivals = 'Sosiri'; static const arrivals = 'Sosiri';
static const departures = 'Pleacări'; static const departures = 'Plecări';
static const loadingText = 'Se încarcă...'; static const loadingText = 'Se încarcă...';
static const arrivesFrom = 'Sosește de la'; static const arrivesFrom = 'Sosește de la';
static const arrivedFrom = 'A sosit de la'; static const arrivedFrom = 'A sosit de la';
@ -63,8 +63,8 @@ abstract class ViewStationPageState extends State<ViewStationPage> {
} }
Widget buildContent(BuildContext context, Future Function() refresh, Future Function(Future<StationData> Function() newFutureBuilder) _, RefreshFutureBuilderSnapshot<StationData> snapshot); Widget buildContent(BuildContext context, Future Function() refresh, Future Function(Future<StationData> Function() newFutureBuilder) _, RefreshFutureBuilderSnapshot<StationData> snapshot);
Widget buildStationArrivalItem(BuildContext context, StationArrival item); Widget buildStationArrivalItem(BuildContext context, StationArrDep item);
Widget buildStationDepartureItem(BuildContext context, StationDeparture item); Widget buildStationDepartureItem(BuildContext context, StationArrDep item);
void onTabChange(int index) { void onTabChange(int index) {
setState(() { setState(() {

9
lib/pages/station_arrdep_page/view_station/view_station_cupertino.dart

@ -1,7 +1,6 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:info_tren/components/loading/loading.dart'; import 'package:info_tren/components/loading/loading.dart';
import 'package:info_tren/components/refresh_future_builder.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/components/sliver_persistent_header_padding.dart';
import 'package:info_tren/models/station_data.dart'; import 'package:info_tren/models/station_data.dart';
import 'package:info_tren/pages/station_arrdep_page/view_station/view_station.dart'; import 'package:info_tren/pages/station_arrdep_page/view_station/view_station.dart';
@ -53,7 +52,7 @@ class ViewStationPageStateCupertino extends ViewStationPageState {
} }
@override @override
Widget buildStationArrivalItem(BuildContext context, StationArrival item) { Widget buildStationArrivalItem(BuildContext context, StationArrDep item) {
return GestureDetector( return GestureDetector(
onTap: () => onTrainTapped(item.train.number), onTap: () => onTrainTapped(item.train.number),
child: CupertinoFormRow( child: CupertinoFormRow(
@ -77,7 +76,7 @@ class ViewStationPageStateCupertino extends ViewStationPageState {
children: [ children: [
TextSpan(text: ViewStationPageState.arrivesFrom), TextSpan(text: ViewStationPageState.arrivesFrom),
TextSpan(text: ' '), TextSpan(text: ' '),
TextSpan(text: item.train.origin), TextSpan(text: item.train.terminus),
], ],
), ),
), ),
@ -86,7 +85,7 @@ class ViewStationPageStateCupertino extends ViewStationPageState {
} }
@override @override
Widget buildStationDepartureItem(BuildContext context, StationDeparture item) { Widget buildStationDepartureItem(BuildContext context, StationArrDep item) {
return GestureDetector( return GestureDetector(
onTap: () => onTrainTapped(item.train.number), onTap: () => onTrainTapped(item.train.number),
child: CupertinoFormRow( child: CupertinoFormRow(
@ -110,7 +109,7 @@ class ViewStationPageStateCupertino extends ViewStationPageState {
children: [ children: [
TextSpan(text: ViewStationPageState.departsTo), TextSpan(text: ViewStationPageState.departsTo),
TextSpan(text: ' '), TextSpan(text: ' '),
TextSpan(text: item.train.destination), TextSpan(text: item.train.terminus),
], ],
), ),
), ),

204
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:flutter/material.dart';
import 'package:info_tren/components/badge.dart';
import 'package:info_tren/components/loading/loading.dart'; import 'package:info_tren/components/loading/loading.dart';
import 'package:info_tren/components/refresh_future_builder.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/models/station_data.dart';
import 'package:info_tren/pages/station_arrdep_page/view_station/view_station.dart'; import 'package:info_tren/pages/station_arrdep_page/view_station/view_station.dart';
@ -48,9 +50,53 @@ class ViewStationPageStateMaterial extends ViewStationPageState {
} }
@override @override
Widget buildStationArrivalItem(BuildContext context, StationArrival item) { Widget buildStationArrivalItem(BuildContext context, StationArrDep item) {
return ListTile( return InkWell(
leading: Text('${item.time.toLocal().hour.toString().padLeft(2, '0')}:${item.time.toLocal().minute.toString().padLeft(2, '0')}'), 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( title: Text.rich(
TextSpan( TextSpan(
children: [ children: [
@ -68,20 +114,108 @@ class ViewStationPageStateMaterial extends ViewStationPageState {
subtitle: Text.rich( subtitle: Text.rich(
TextSpan( TextSpan(
children: [ children: [
TextSpan(text: item.time.compareTo(DateTime.now()) < 0 ? ViewStationPageState.arrivedFrom : ViewStationPageState.arrivesFrom), 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: ' '), TextSpan(text: ' '),
TextSpan(text: item.train.origin), 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,
),
),
],
], ],
), ),
), ),
onTap: () => onTrainTapped(item.train.number), ),
),
),
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,
),
),
),
],
),
); );
} }
@override @override
Widget buildStationDepartureItem(BuildContext context, StationDeparture item) { Widget buildStationDepartureItem(BuildContext context, StationArrDep item) {
return ListTile( return InkWell(
leading: Text('${item.time.toLocal().hour.toString().padLeft(2, '0')}:${item.time.toLocal().minute.toString().padLeft(2, '0')}'), 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( title: Text.rich(
TextSpan( TextSpan(
children: [ children: [
@ -99,13 +233,57 @@ class ViewStationPageStateMaterial extends ViewStationPageState {
subtitle: Text.rich( subtitle: Text.rich(
TextSpan( TextSpan(
children: [ children: [
TextSpan(text: item.time.compareTo(DateTime.now()) < 0 ? ViewStationPageState.departedTo : ViewStationPageState.departsTo), 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: ' '), TextSpan(text: ' '),
TextSpan(text: item.train.destination), 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,
),
),
],
], ],
), ),
), ),
onTap: () => onTrainTapped(item.train.number), ),
),
),
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,
),
),
),
],
),
); );
} }
} }

2
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. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 2.7.6 version: 2.7.7
environment: environment:
sdk: ">=2.15.0 <3.0.0" sdk: ">=2.15.0 <3.0.0"

Loading…
Cancel
Save