import 'dart:async'; import 'dart:convert'; import 'package:flutter/widgets.dart'; import 'package:http/http.dart' as http; import 'package:json_annotation/json_annotation.dart'; import 'package:rxdart/subjects.dart'; part 'train_data.g.dart'; class TrainDataSourceWidget extends InheritedWidget { final TrainDataSource dataSource; @override bool updateShouldNotify(InheritedWidget oldWidget) { return true; } TrainDataSourceWidget({Widget child, Key key}) : dataSource = TrainDataSource(), super(key: key, child: child); static TrainDataSource of(BuildContext context) { return (context.inheritFromWidgetOfExactType(TrainDataSourceWidget) as TrainDataSourceWidget).dataSource; } } class TrainDataSource { Future lookup(String trainNumber, [bool alternate = false]) async { _controller.add( TrainDataEvent( trainNumber: trainNumber, kind: TrainDataEventKind.LOADING ) ); try { final response = await http.get( "http://cfr-scrapper.herokuapp.com/train/$trainNumber${alternate ? "?alt" : ""}", headers: { "Authorization": "Flutter - Info Tren", "X-Issued-By": "Flutter App" } ); if (200 <= response.statusCode && response.statusCode <= 299) { // OK final text = response.body; final ser = json.decode(text ); final data = TrainData.fromJson(ser ); _controller.add( TrainDataEvent( kind: TrainDataEventKind.GOT_DATA, trainNumber: trainNumber, trainData: data ) ); } else if (response.statusCode == 404) { _controller.addError( TrainDataEvent( kind: TrainDataEventKind.NOT_FOUND, trainNumber: trainNumber, errorStatusCode: 404 ) ); } else if (response.statusCode == 503) { _controller.addError( TrainDataEvent( kind: TrainDataEventKind.TIMEOUT, trainNumber: trainNumber, errorStatusCode: 503 ) ); } else { // Throw error _controller.addError( TrainDataEvent( kind: TrainDataEventKind.UNKNOWN_ERROR, trainNumber: trainNumber, errorStatusCode: response.statusCode ) ); } } catch (e) { _controller.addError( TrainDataEvent( kind: TrainDataEventKind.UNKNOWN_ERROR, trainNumber: trainNumber, errorStatusCode: -1 ) ); } } TrainDataSource() { _controller = BehaviorSubject(); } dispose() { _controller.close(); } BehaviorSubject _controller; Stream get dataStream => _controller.stream; } enum TrainDataEventKind { LOADING, TIMEOUT, NOT_FOUND, UNKNOWN_ERROR, GOT_DATA } class TrainDataEvent { final TrainData trainData; final TrainDataEventKind kind; final String trainNumber; final int errorStatusCode; TrainDataEvent({this.kind, this.trainData, this.trainNumber, this.errorStatusCode}); } @JsonSerializable() class TrainData { final String rang; @JsonKey(name: "tren") final String trainNumber; final String operator; @JsonKey(name: "relatia") final String route; @JsonKey(name: "stare") final String state; @JsonKey(name: "ultima_informatie") final LastInfo lastInfo; @JsonKey(name: "destinatie") final StopInfo destination; @JsonKey(name: "urmatoarea_oprire") final StopInfo nextStop; @JsonKey(name: "durata_calatoriei") final String tripLength; @JsonKey(name: "distanta") final String distance; @JsonKey(name: "stations") List stations; TrainData({this.rang, this.trainNumber, this.operator, this.lastInfo, this.state, this.route, this.tripLength, this.stations, this.nextStop, this.distance, this.destination}); factory TrainData.fromJson(Map json) { var result = _$TrainDataFromJson(json); var foundEstimat = false; result.stations = result.stations.map((station) { if (station.realOrEstimate == "Estimat") { foundEstimat = true; } station.realOrEstimate = foundEstimat ? "Estimat" : "Real"; return station; }).toList(); return result; } Map toJson() => _$TrainDataToJson(this); } @JsonSerializable() class LastInfo { @JsonKey(name: "statia") final String station; @JsonKey(name: "eveniment") final String event; @JsonKey(name: "data_si_ora") final String dateAndTime; DateTime get formattedDateAndTime { return parseCFRDateTime(dateAndTime); } @JsonKey(name: "intarziere") final int delay; LastInfo({this.dateAndTime, this.delay, this.event, this.station}); factory LastInfo.fromJson(Map json) => _$LastInfoFromJson(json); Map toJson() => _$LastInfoToJson(this); } @JsonSerializable() class StopInfo { @JsonKey(name: "statia") final String station; @JsonKey(name: "data_si_ora") final String dateAndTime; DateTime get formattedDateAndTime { return parseCFRDateTime(dateAndTime); } StopInfo({this.station, this.dateAndTime}); factory StopInfo.fromJson(Map json) => _$StopInfoFromJson(json); Map toJson() => _$StopInfoToJson(this); } @JsonSerializable() class StationEntry { final String km; @JsonKey(name: "statia") final String name; @JsonKey(name: "sosire") final String arrivalTime; @JsonKey(name: "stationeaza_pentru") final String waitTime; @JsonKey(name: "plecare") final String departureTime; @JsonKey(name: "real/estimat") String realOrEstimate; bool get real { return realOrEstimate == "Real"; } @JsonKey(name: "intarziere") final int delay; @JsonKey(name: "observatii") final String observations; StationEntry({this.name, this.delay, this.realOrEstimate, this.arrivalTime, this.departureTime, this.km, this.observations, this.waitTime}); factory StationEntry.fromJson(Map json) => _$StationEntryFromJson(json); Map toJson() => _$StationEntryToJson(this); } DateTime parseCFRDateTime(String dateAndTime) { final parts = dateAndTime.split(" "); final dateParts = parts[0].split("."); final day = int.parse(dateParts[0]); final month = int.parse(dateParts[1]); final year = int.parse(dateParts[2]); final timeParts = parts[1].split(":"); final hour = int.parse(timeParts[0]); final minute = int.parse(timeParts[1]); return DateTime(year, month, day, hour, minute); }