diff --git a/CHANGELOG.TXT b/CHANGELOG.TXT index 3432f03..c89f936 100644 --- a/CHANGELOG.TXT +++ b/CHANGELOG.TXT @@ -1,3 +1,6 @@ +v2.5.0 +Initial arrivals/departures support + v2.4.1 Fixed DateTime (UTC -> local) diff --git a/lib/api/common.dart b/lib/api/common.dart new file mode 100644 index 0000000..ddbcca1 --- /dev/null +++ b/lib/api/common.dart @@ -0,0 +1 @@ +const authority = 'scraper.infotren.dcdevelop.xyz'; \ No newline at end of file diff --git a/lib/api/station_data.dart b/lib/api/station_data.dart new file mode 100644 index 0000000..3d1ea9c --- /dev/null +++ b/lib/api/station_data.dart @@ -0,0 +1,10 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +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')); + return StationData.fromJson(jsonDecode(response.body)); +} \ No newline at end of file diff --git a/lib/api/stations.dart b/lib/api/stations.dart new file mode 100644 index 0000000..cc51898 --- /dev/null +++ b/lib/api/stations.dart @@ -0,0 +1,11 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:info_tren/api/common.dart'; +import 'package:info_tren/models/stations_result.dart'; + +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/api/train_data.dart b/lib/api/train_data.dart index c05fd4b..da373f9 100644 --- a/lib/api/train_data.dart +++ b/lib/api/train_data.dart @@ -1,9 +1,8 @@ import 'package:http/http.dart' as http; +import 'package:info_tren/api/common.dart'; import 'package:info_tren/models/train_data.dart'; -const AUTHORITY = 'scraper.infotren.dcdevelop.xyz'; - Future getTrain(String trainNumber) async { - final response = await http.get(Uri.https(AUTHORITY, 'v2/train/$trainNumber')); + final response = await http.get(Uri.https(authority, 'v2/train/$trainNumber')); return trainDataFromJson(response.body); } \ No newline at end of file diff --git a/lib/components/cupertino_listtile.dart b/lib/components/cupertino_listtile.dart new file mode 100644 index 0000000..030c430 --- /dev/null +++ b/lib/components/cupertino_listtile.dart @@ -0,0 +1,43 @@ +import 'package:flutter/cupertino.dart'; + +class CupertinoListTile extends StatelessWidget { + final Widget? leading; + final Widget? title; + final Widget? subtitle; + final Widget? trailing; + + const CupertinoListTile({ Key? key, this.leading, this.title, this.subtitle, this.trailing, }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + if (leading != null) + leading!, + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (title != null) + title!, + if (subtitle != null) + CupertinoTheme( + child: subtitle!, + data: CupertinoTheme.of(context).copyWith( + textTheme: CupertinoTextThemeData( + textStyle: TextStyle( + fontSize: CupertinoTheme.of(context).textTheme.textStyle.fontSize! - 2, + ) + ) + ), + ), + ], + ), + ), + if (trailing != null) + trailing!, + ], + ); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 7617d33..35ed5c5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; // import 'package:flutter_redux/flutter_redux.dart'; import 'package:info_tren/models/ui_design.dart'; import 'package:info_tren/pages/main/main_page.dart'; +import 'package:info_tren/pages/station_arrdep_page/select_station/select_station.dart'; +import 'package:info_tren/pages/station_arrdep_page/view_station/view_station.dart'; import 'package:info_tren/pages/train_info_page/view_train/train_info.dart'; import 'package:info_tren/pages/train_info_page/select_train/select_train.dart'; @@ -36,6 +38,15 @@ Map routesByUiDesign(UiDesign uiDesign) => { uiDesign: uiDesign, ); }, + SelectStationPage.routeName: (context) { + return SelectStationPage(uiDesign: uiDesign,); + }, + ViewStationPage.routeName: (context) { + return ViewStationPage( + stationName: ModalRoute.of(context)!.settings.arguments as String, + uiDesign: uiDesign, + ); + }, }; class StartPoint extends StatelessWidget { diff --git a/lib/models/station_data.dart b/lib/models/station_data.dart new file mode 100644 index 0000000..db614dd --- /dev/null +++ b/lib/models/station_data.dart @@ -0,0 +1,68 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'station_data.g.dart'; + +@JsonSerializable() +class StationData { + final String date; + final String stationName; + final List? arrivals; + final List? departures; + + const StationData({required this.date, required this.stationName, required this.arrivals, required this.departures}); + + factory StationData.fromJson(Map json) => _$StationDataFromJson(json); + Map toJson() => _$StationDataToJson(this); +} + +@JsonSerializable() +class StationArrival { + final int? stoppingTime; + final DateTime time; + final StationTrainArr train; + + const StationArrival({required this.stoppingTime, required this.time, required this.train,}); + + factory StationArrival.fromJson(Map json) => _$StationArrivalFromJson(json); + Map toJson() => _$StationArrivalToJson(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 { + final String rank; + final String number; + final String operator; + final String origin; + final List? route; + + StationTrainArr({required this.rank, required this.number, required this.operator, required this.origin, this.route,}); + + factory StationTrainArr.fromJson(Map json) => _$StationTrainArrFromJson(json); + Map toJson() => _$StationTrainArrToJson(this); +} + +@JsonSerializable() +class StationTrainDep { + final String rank; + final String number; + final String operator; + final String destination; + final List? route; + + StationTrainDep({required this.rank, required this.number, required this.operator, required this.destination, this.route,}); + + factory StationTrainDep.fromJson(Map json) => _$StationTrainDepFromJson(json); + Map toJson() => _$StationTrainDepToJson(this); +} diff --git a/lib/models/station_data.g.dart b/lib/models/station_data.g.dart new file mode 100644 index 0000000..d2f7964 --- /dev/null +++ b/lib/models/station_data.g.dart @@ -0,0 +1,92 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'station_data.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +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)) + .toList(), + departures: (json['departures'] as List?) + ?.map((e) => StationDeparture.fromJson(e as Map)) + .toList(), + ); + +Map _$StationDataToJson(StationData instance) => + { + 'date': instance.date, + 'stationName': instance.stationName, + 'arrivals': instance.arrivals, + 'departures': instance.departures, + }; + +StationArrival _$StationArrivalFromJson(Map json) => + StationArrival( + stoppingTime: json['stoppingTime'] as int?, + time: DateTime.parse(json['time'] as String), + train: StationTrainArr.fromJson(json['train'] as Map), + ); + +Map _$StationArrivalToJson(StationArrival instance) => + { + 'stoppingTime': instance.stoppingTime, + 'time': instance.time.toIso8601String(), + 'train': instance.train, + }; + +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( + rank: json['rank'] as String, + number: json['number'] as String, + operator: json['operator'] as String, + origin: json['origin'] as String, + route: + (json['route'] as List?)?.map((e) => e as String).toList(), + ); + +Map _$StationTrainArrToJson(StationTrainArr instance) => + { + 'rank': instance.rank, + 'number': instance.number, + 'operator': instance.operator, + 'origin': instance.origin, + '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(), + ); + +Map _$StationTrainDepToJson(StationTrainDep instance) => + { + 'rank': instance.rank, + 'number': instance.number, + 'operator': instance.operator, + 'destination': instance.destination, + 'route': instance.route, + }; diff --git a/lib/models/stations_result.dart b/lib/models/stations_result.dart new file mode 100644 index 0000000..8cde170 --- /dev/null +++ b/lib/models/stations_result.dart @@ -0,0 +1,14 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'stations_result.g.dart'; + +@JsonSerializable() +class StationsResult { + final String name; + final List? stoppedAtBy; + + const StationsResult({required this.name, this.stoppedAtBy}); + + factory StationsResult.fromJson(Map json) => _$StationsResultFromJson(json); + Map toJson() => _$StationsResultToJson(this); +} \ No newline at end of file diff --git a/lib/models/stations_result.g.dart b/lib/models/stations_result.g.dart new file mode 100644 index 0000000..5b05ac6 --- /dev/null +++ b/lib/models/stations_result.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'stations_result.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +StationsResult _$StationsResultFromJson(Map json) => + StationsResult( + name: json['name'] as String, + stoppedAtBy: (json['stoppedAtBy'] as List?) + ?.map((e) => e as String) + .toList(), + ); + +Map _$StationsResultToJson(StationsResult instance) => + { + 'name': instance.name, + 'stoppedAtBy': instance.stoppedAtBy, + }; diff --git a/lib/pages/main/main_page.dart b/lib/pages/main/main_page.dart index 6734556..6911b3e 100644 --- a/lib/pages/main/main_page.dart +++ b/lib/pages/main/main_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:info_tren/models/ui_design.dart'; import 'package:info_tren/pages/main/main_page_cupertino.dart'; import 'package:info_tren/pages/main/main_page_material.dart'; +import 'package:info_tren/pages/station_arrdep_page/select_station/select_station.dart'; import 'package:info_tren/pages/train_info_page/select_train/select_train.dart'; import 'package:info_tren/utils/default_ui_design.dart'; @@ -37,8 +38,9 @@ abstract class MainPageShared extends StatelessWidget { ), MainPageOption( name: 'Tabelă plecari/sosiri', - // TODO: Implement departure/arrival - action: null, + action: (context) { + onStationBoardPageInvoke(context); + }, ), MainPageOption( name: 'Planificare rută', @@ -52,7 +54,7 @@ abstract class MainPageShared extends StatelessWidget { } onStationBoardPageInvoke(BuildContext context) { - + Navigator.of(context).pushNamed(SelectStationPage.routeName); } onRoutePlanPageInvoke(BuildContext context) { diff --git a/lib/pages/station_arrdep_page/select_station/select_station.dart b/lib/pages/station_arrdep_page/select_station/select_station.dart index 45e7507..3992dd0 100644 --- a/lib/pages/station_arrdep_page/select_station/select_station.dart +++ b/lib/pages/station_arrdep_page/select_station/select_station.dart @@ -2,7 +2,9 @@ import 'package:flutter/widgets.dart'; import 'package:info_tren/models/ui_design.dart'; import 'package:info_tren/pages/station_arrdep_page/select_station/select_station_cupertino.dart'; import 'package:info_tren/pages/station_arrdep_page/select_station/select_station_material.dart'; +import 'package:info_tren/pages/station_arrdep_page/view_station/view_station.dart'; import 'package:info_tren/utils/default_ui_design.dart'; +import 'package:info_tren/api/stations.dart' as apiStations; class SelectStationPage extends StatefulWidget { final UiDesign? uiDesign; @@ -24,6 +26,55 @@ class SelectStationPage extends StatefulWidget { } abstract class SelectStationPageState extends State { + static const pageTitle = 'Plecări/sosiri stație'; + static const textFieldLabel = 'Numele stației'; + static const roToEn = { + 'ă': 'a', + 'â': 'a', + 'î': 'i', + 'ș': 's', + 'ț': 't', + }; + List stations = []; + + List get filteredStations { + final filter = textEditingController.text + .trim() + .toLowerCase() + .replaceAllMapped(RegExp(r'[ăâîșț]'), (match) => roToEn[match[0]!]!); + if (filter.isEmpty) { + return stations; + } + + return stations.where( + (e) => e + .toLowerCase() + .replaceAllMapped(RegExp(r'[ăâîșț]'), (match) { + return roToEn[match[0]!]!; + }) + .contains(filter) + ).toList(growable: false); + } + + @override + void initState() { + apiStations.stations.then((value) { + setState(() { + stations = value.map((e) => e.name).toList(growable: false,); + }); + }); + + super.initState(); + } + + void onTextChanged(String newText) { + setState(() {}); + } + + void onSuggestionSelected(String suggestion) { + Navigator.of(context).pushNamed(ViewStationPage.routeName, arguments: suggestion); + } + final TextEditingController textEditingController = TextEditingController(); } diff --git a/lib/pages/station_arrdep_page/select_station/select_station_cupertino.dart b/lib/pages/station_arrdep_page/select_station/select_station_cupertino.dart index aae0bea..80e8516 100644 --- a/lib/pages/station_arrdep_page/select_station/select_station_cupertino.dart +++ b/lib/pages/station_arrdep_page/select_station/select_station_cupertino.dart @@ -1,9 +1,53 @@ import 'package:flutter/cupertino.dart'; +import 'package:info_tren/components/cupertino_divider.dart'; import 'package:info_tren/pages/station_arrdep_page/select_station/select_station.dart'; class SelectStationPageStateCupertino extends SelectStationPageState { @override Widget build(BuildContext context) { - return Container(); + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(SelectStationPageState.pageTitle), + ), + child: SafeArea( + bottom: false, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: CupertinoTextField( + controller: textEditingController, + autofocus: true, + placeholder: SelectStationPageState.textFieldLabel, + textInputAction: TextInputAction.search, + onChanged: onTextChanged, + ), + ), + Expanded( + child: ListView.builder( + itemBuilder: (context, index) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + GestureDetector( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(filteredStations[index]), + ), + onTap: () => onSuggestionSelected(filteredStations[index]), + ), + CupertinoDivider(), + ], + ); + }, + itemCount: filteredStations.length, + ), + ), + ], + ), + ), + ); } } diff --git a/lib/pages/station_arrdep_page/select_station/select_station_material.dart b/lib/pages/station_arrdep_page/select_station/select_station_material.dart index ae01daa..7d09fad 100644 --- a/lib/pages/station_arrdep_page/select_station/select_station_material.dart +++ b/lib/pages/station_arrdep_page/select_station/select_station_material.dart @@ -5,6 +5,52 @@ import 'package:info_tren/pages/station_arrdep_page/select_station/select_statio class SelectStationPageStateMaterial extends SelectStationPageState { @override Widget build(BuildContext context) { - return Container(); + return Scaffold( + appBar: AppBar( + title: Text(SelectStationPageState.pageTitle), + centerTitle: true, + ), + body: SafeArea( + bottom: false, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: TextField( + controller: textEditingController, + autofocus: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: SelectStationPageState.textFieldLabel, + ), + textInputAction: TextInputAction.search, + onChanged: onTextChanged, + ), + ), + Expanded( + child: ListView.builder( + itemBuilder: (context, index) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + dense: true, + title: Text(filteredStations[index]), + onTap: () => onSuggestionSelected(filteredStations[index]), + ), + Divider( + height: 1, + ), + ], + ); + }, + itemCount: filteredStations.length, + ), + ), + ], + ), + ), + ); } } diff --git a/lib/pages/station_arrdep_page/view_station/view_station.dart b/lib/pages/station_arrdep_page/view_station/view_station.dart new file mode 100644 index 0000000..264400b --- /dev/null +++ b/lib/pages/station_arrdep_page/view_station/view_station.dart @@ -0,0 +1,89 @@ +import 'package:flutter/widgets.dart'; +import 'package:info_tren/api/station_data.dart'; +import 'package:info_tren/components/refresh_future_builder.dart'; +import 'package:info_tren/models/station_data.dart'; +import 'package:info_tren/models/ui_design.dart'; +import 'package:info_tren/pages/station_arrdep_page/view_station/view_station_cupertino.dart'; +import 'package:info_tren/pages/station_arrdep_page/view_station/view_station_material.dart'; +import 'package:info_tren/pages/train_info_page/view_train/train_info.dart'; +import 'package:info_tren/utils/default_ui_design.dart'; + +class ViewStationPage extends StatefulWidget { + final UiDesign? uiDesign; + final String stationName; + + const ViewStationPage({ Key? key, required this.stationName, this.uiDesign }) : super(key: key); + + static String routeName = '/stationArrDep/viewStation'; + + @override + ViewStationPageState createState() { + final uiDesign = this.uiDesign ?? defaultUiDesign; + switch (uiDesign) { + case UiDesign.MATERIAL: + return ViewStationPageStateMaterial(); + case UiDesign.CUPERTINO: + return ViewStationPageStateCupertino(); + } + } +} + +abstract class ViewStationPageState extends State { + static const arrivals = 'Sosiri'; + static const departures = 'Pleacări'; + static const loadingText = 'Se încarcă...'; + static const arrivesFrom = 'Sosește de la'; + static const departsTo = 'Pleacă către'; + + ViewStationPageTab tab = ViewStationPageTab.departures; + late String stationName; + late Future Function() futureCreator; + + @override + void initState() { + initData(); + super.initState(); + } + + @override + void didChangeDependencies() { + if (stationName != widget.stationName) { + setState(() { + initData(); + }); + } + super.didChangeDependencies(); + } + + void initData() { + stationName = widget.stationName; + futureCreator = () => getStationData(stationName); + } + + Widget buildContent(BuildContext context, Future Function() refresh, RefreshFutureBuilderSnapshot snapshot); + Widget buildStationArrivalItem(BuildContext context, StationArrival item); + Widget buildStationDepartureItem(BuildContext context, StationDeparture item); + + void onTabChange(int index) { + setState(() { + tab = ViewStationPageTab.values[index]; + }); + } + + void onTrainTapped(String trainNumber) { + Navigator.of(context).pushNamed(TrainInfo.routeName, arguments: trainNumber); + } + + @override + Widget build(BuildContext context) { + return RefreshFutureBuilder( + futureCreator: futureCreator, + builder: buildContent, + ); + } +} + +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 new file mode 100644 index 0000000..e497262 --- /dev/null +++ b/lib/pages/station_arrdep_page/view_station/view_station_cupertino.dart @@ -0,0 +1,120 @@ +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'; + +class ViewStationPageStateCupertino extends ViewStationPageState { + @override + Widget buildContent(BuildContext context, Future Function() refresh, RefreshFutureBuilderSnapshot snapshot) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(snapshot.hasData ? snapshot.data!.stationName : stationName), + ), + child: snapshot.hasData ? CupertinoTabScaffold( + tabBar: CupertinoTabBar( + items: [ + BottomNavigationBarItem( + icon: Icon(CupertinoIcons.arrow_down), + label: ViewStationPageState.arrivals, + ), + BottomNavigationBarItem( + icon: Icon(CupertinoIcons.arrow_up), + label: ViewStationPageState.departures, + ), + ], + onTap: onTabChange, + currentIndex: tab.index, + ), + tabBuilder: (context, index) { + final topPadding = MediaQuery.of(context).padding.top; + return NestedScrollView( + headerSliverBuilder: (context, _) { + return [SliverPersistentHeaderPadding(maxHeight: topPadding)]; + }, + body: CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return tab == ViewStationPageTab.arrivals ? buildStationArrivalItem(context, snapshot.data!.arrivals![index]) : buildStationDepartureItem(context, snapshot.data!.departures![index]); + }, + childCount: tab == ViewStationPageTab.arrivals ? snapshot.data!.arrivals?.length ?? 0 : snapshot.data!.departures?.length ?? 0, + ), + ), + ], + ), + ); + }, + ) : snapshot.state == RefreshFutureBuilderState.waiting ? Loading(text: ViewStationPageState.loadingText, uiDesign: widget.uiDesign,) : Container(), + ); + } + + @override + Widget buildStationArrivalItem(BuildContext context, StationArrival item) { + return GestureDetector( + onTap: () => onTrainTapped(item.train.number), + child: CupertinoFormRow( + child: Text('${item.time.toLocal().hour.toString().padLeft(2, '0')}:${item.time.toLocal().minute.toString().padLeft(2, '0')}'), + prefix: 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,), + ], + ), + ), + helper: Text.rich( + TextSpan( + children: [ + TextSpan(text: ViewStationPageState.arrivesFrom), + TextSpan(text: ' '), + TextSpan(text: item.train.origin), + ], + ), + ), + ), + ); + } + + @override + Widget buildStationDepartureItem(BuildContext context, StationDeparture item) { + return GestureDetector( + onTap: () => onTrainTapped(item.train.number), + child: CupertinoFormRow( + child: Text('${item.time.toLocal().hour.toString().padLeft(2, '0')}:${item.time.toLocal().minute.toString().padLeft(2, '0')}'), + prefix: 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,), + ], + ), + ), + helper: Text.rich( + TextSpan( + children: [ + TextSpan(text: ViewStationPageState.departsTo), + TextSpan(text: ' '), + TextSpan(text: item.train.destination), + ], + ), + ), + ), + ); + } +} \ 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 new file mode 100644 index 0000000..4c5202d --- /dev/null +++ b/lib/pages/station_arrdep_page/view_station/view_station_material.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.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'; + +class ViewStationPageStateMaterial extends ViewStationPageState { + @override + Widget buildContent(BuildContext context, Future Function() refresh, RefreshFutureBuilderSnapshot snapshot) { + return Scaffold( + appBar: AppBar( + title: Text(snapshot.hasData ? snapshot.data!.stationName : stationName), + centerTitle: true, + ), + body: snapshot.state == RefreshFutureBuilderState.waiting ? Loading(text: ViewStationPageState.loadingText, uiDesign: widget.uiDesign,) : CustomScrollView( + slivers: [ + SliverToBoxAdapter(child: SafeArea(child: Container(), left: false, bottom: false, right: false,),), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return tab == ViewStationPageTab.arrivals ? buildStationArrivalItem(context, snapshot.data!.arrivals![index]) : buildStationDepartureItem(context, snapshot.data!.departures![index]); + }, + childCount: tab == ViewStationPageTab.arrivals ? snapshot.data!.arrivals?.length ?? 0 : snapshot.data!.departures?.length ?? 0, + ), + ), + ], + ), + bottomNavigationBar: snapshot.hasData ? BottomNavigationBar( + items: [ + BottomNavigationBarItem( + icon: Icon(Icons.arrow_downward), + label: ViewStationPageState.arrivals, + ), + BottomNavigationBarItem( + icon: Icon(Icons.arrow_upward), + label: ViewStationPageState.departures, + ), + ], + currentIndex: tab.index, + onTap: onTabChange, + ) : null, + ); + } + + @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, + ), + ), + TextSpan(text: ' '), + TextSpan(text: item.train.number,), + ], + ), + ), + subtitle: Text.rich( + TextSpan( + children: [ + TextSpan(text: ViewStationPageState.arrivesFrom), + TextSpan(text: ' '), + TextSpan(text: item.train.origin), + ], + ), + ), + 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, + ), + ), + TextSpan(text: ' '), + TextSpan(text: item.train.number,), + ], + ), + ), + subtitle: Text.rich( + TextSpan( + children: [ + TextSpan(text: ViewStationPageState.departsTo), + TextSpan(text: ' '), + TextSpan(text: item.train.destination), + ], + ), + ), + onTap: () => onTrainTapped(item.train.number), + ); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 3c84e73..cc37d6c 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.4.1 +version: 2.5.0 environment: sdk: ">=2.12.0 <3.0.0"