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. 8
      CHANGELOG.txt
  2. 4
      lib/api/station_data.dart
  3. 2
      lib/api/stations.dart
  4. 53
      lib/models/station_data.dart
  5. 60
      lib/models/station_data.g.dart
  6. 8
      lib/pages/station_arrdep_page/view_station/view_station.dart
  7. 11
      lib/pages/station_arrdep_page/view_station/view_station_cupertino.dart
  8. 278
      lib/pages/station_arrdep_page/view_station/view_station_material.dart
  9. 2
      pubspec.yaml

8
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
- uses WebView to get data on device instead of relying on server

4
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<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));
}
}

2
lib/api/stations.dart

@ -8,4 +8,4 @@ Future<List<StationsResult>> get stations async {
final result = await http.get(Uri.https(authority, 'v2/stations'));
final data = jsonDecode(result.body) as List<dynamic>;
return data.map((e) => StationsResult.fromJson(e)).toList(growable: false,);
}
}

53
lib/models/station_data.dart

@ -6,8 +6,8 @@ part 'station_data.g.dart';
class StationData {
final String date;
final String stationName;
final List<StationArrival>? arrivals;
final List<StationDeparture>? departures;
final List<StationArrDep>? arrivals;
final List<StationArrDep>? 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<String, dynamic> json) => _$StationArrivalFromJson(json);
Map<String, dynamic> toJson() => _$StationArrivalToJson(this);
factory StationArrDep.fromJson(Map<String, dynamic> json) => _$StationArrDepFromJson(json);
Map<String, dynamic> 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<String, dynamic> json) => _$StationDepartureFromJson(json);
Map<String, dynamic> 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<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);
Map<String, dynamic> toJson() => _$StationTrainArrToJson(this);
factory StationTrain.fromJson(Map<String, dynamic> json) => _$StationTrainFromJson(json);
Map<String, dynamic> toJson() => _$StationTrainToJson(this);
}
@JsonSerializable()
class StationTrainDep {
final String rank;
final String number;
final String operator;
final String destination;
final List<String>? 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<String, dynamic> json) => _$StationTrainDepFromJson(json);
Map<String, dynamic> toJson() => _$StationTrainDepToJson(this);
factory StationStatus.fromJson(Map<String, dynamic> json) => _$StationStatusFromJson(json);
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,
stationName: json['stationName'] as String,
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(),
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(),
);
@ -25,68 +25,50 @@ Map<String, dynamic> _$StationDataToJson(StationData instance) =>
'departures': instance.departures,
};
StationArrival _$StationArrivalFromJson(Map<String, dynamic> json) =>
StationArrival(
StationArrDep _$StationArrDepFromJson(Map<String, dynamic> json) =>
StationArrDep(
stoppingTime: json['stoppingTime'] as int?,
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>{
'stoppingTime': instance.stoppingTime,
'time': instance.time.toIso8601String(),
'train': instance.train,
'status': instance.status,
};
StationDeparture _$StationDepartureFromJson(Map<String, dynamic> json) =>
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(
StationTrain _$StationTrainFromJson(Map<String, dynamic> 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<dynamic>?)?.map((e) => e as String).toList(),
);
Map<String, dynamic> _$StationTrainArrToJson(StationTrainArr instance) =>
Map<String, dynamic> _$StationTrainToJson(StationTrain instance) =>
<String, dynamic>{
'rank': instance.rank,
'number': instance.number,
'operator': instance.operator,
'origin': instance.origin,
'terminus': instance.terminus,
'route': instance.route,
};
StationTrainDep _$StationTrainDepFromJson(Map<String, dynamic> 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<dynamic>?)?.map((e) => e as String).toList(),
StationStatus _$StationStatusFromJson(Map<String, dynamic> json) =>
StationStatus(
delay: json['delay'] as int,
real: json['real'] as bool,
platform: json['platform'] as String?,
);
Map<String, dynamic> _$StationTrainDepToJson(StationTrainDep instance) =>
Map<String, dynamic> _$StationStatusToJson(StationStatus instance) =>
<String, dynamic>{
'rank': instance.rank,
'number': instance.number,
'operator': instance.operator,
'destination': instance.destination,
'route': instance.route,
'delay': instance.delay,
'real': instance.real,
'platform': instance.platform,
};

8
lib/pages/station_arrdep_page/view_station/view_station.dart

@ -30,7 +30,7 @@ class ViewStationPage extends StatefulWidget {
abstract class ViewStationPageState extends State<ViewStationPage> {
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<ViewStationPage> {
}
Widget buildContent(BuildContext context, Future Function() refresh, Future Function(Future<StationData> Function() newFutureBuilder) _, RefreshFutureBuilderSnapshot<StationData> 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<ViewStationPage> {
enum ViewStationPageTab {
arrivals,
departures,
}
}

11
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),
],
),
),
),
);
}
}
}

278
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),
);
}
}
}

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.
# 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"

Loading…
Cancel
Save