commit 30210f4447b1417aec5425beec34badd91640bff
parent 9bfdac4fd5cfdd93e7e498cfb2623cd5414d7411
Author: Marcel <mtrnord1@gmail.com>
Date: Sat, 22 Feb 2020 23:43:16 +0100
Add first map version and prepare login
Took 2 hours 26 minutes
Diffstat:
30 files changed, 1075 insertions(+), 30 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
@@ -38,7 +38,7 @@ android {
defaultConfig {
applicationId "de.bahnhoefe.deutschlands.bahnhofsfotos"
- minSdkVersion 16
+ minSdkVersion 20
targetSdkVersion 29
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
+
<application
android:name="io.flutter.app.FlutterApplication"
android:label="Bahnhofsfotos"
diff --git a/lib/blocs/AuthenticationBloc.dart b/lib/blocs/AuthenticationBloc.dart
@@ -0,0 +1,44 @@
+import 'package:bloc/bloc.dart';
+import 'package:flutter/cupertino.dart';
+
+import '../repos/UserRepository.dart';
+import '../states/AuthenticationState.dart';
+import '../events/AuthenticationEvent.dart';
+
+class AuthenticationBloc
+ extends Bloc<AuthenticationEvent, AuthenticationState> {
+ final UserRepository userRepository;
+
+ AuthenticationBloc({@required this.userRepository})
+ : assert(userRepository != null);
+
+ @override
+ AuthenticationState get initialState => AuthenticationUninitialized();
+
+ @override
+ Stream<AuthenticationState> mapEventToState(
+ AuthenticationEvent event,
+ ) async* {
+ if (event is AppStarted) {
+ final bool hasToken = await userRepository.hasToken();
+
+ if (hasToken) {
+ yield AuthenticationAuthenticated();
+ } else {
+ yield AuthenticationNeeded();
+ }
+ }
+
+ if (event is LoggedIn) {
+ yield AuthenticationLoading();
+ await userRepository.persistToken(event.token);
+ yield AuthenticationAuthenticated();
+ }
+
+ if (event is LoggedOut) {
+ yield AuthenticationLoading();
+ await userRepository.deleteToken();
+ yield AuthenticationNeeded();
+ }
+ }
+}
diff --git a/lib/blocs/LoginBloc.dart b/lib/blocs/LoginBloc.dart
@@ -0,0 +1,42 @@
+import 'dart:async';
+
+import 'package:bloc/bloc.dart';
+import 'package:meta/meta.dart';
+
+import '../events/LoginEvent.dart';
+import '../events/AuthenticationEvent.dart';
+import '../repos/UserRepository.dart';
+import '../states/LoginState.dart';
+import 'AuthenticationBloc.dart';
+
+class LoginBloc extends Bloc<LoginEvent, LoginState> {
+ final UserRepository userRepository;
+ final AuthenticationBloc authenticationBloc;
+
+ LoginBloc({
+ @required this.userRepository,
+ @required this.authenticationBloc,
+ }) : assert(userRepository != null),
+ assert(authenticationBloc != null);
+
+ LoginState get initialState => LoginInitial();
+
+ @override
+ Stream<LoginState> mapEventToState(LoginEvent event) async* {
+ if (event is LoginButtonPressed) {
+ yield LoginLoading();
+
+ try {
+ final token = await userRepository.authenticate(
+ username: event.username,
+ password: event.password,
+ );
+
+ authenticationBloc.add(LoggedIn(token: token));
+ yield LoginInitial();
+ } catch (error) {
+ yield LoginFailure(error: error.toString());
+ }
+ }
+ }
+}
diff --git a/lib/blocs/MapBloc.dart b/lib/blocs/MapBloc.dart
@@ -0,0 +1,33 @@
+import 'package:bloc/bloc.dart';
+import 'package:flutter/cupertino.dart';
+
+import '../events/MapEvent.dart';
+import '../models/models.dart';
+import '../repos/repositories.dart';
+import '../states/MapState.dart';
+
+class MapBloc extends Bloc<MapEvent, MapState> {
+ final RailwayStationsRepository railwayStationsRepository;
+
+ MapBloc({@required this.railwayStationsRepository})
+ : assert(railwayStationsRepository != null);
+
+ @override
+ MapState get initialState => MapLoading();
+
+ @override
+ Stream<MapState> mapEventToState(MapEvent event) async* {
+ if (event is FetchStations) {
+ yield MapLoading();
+ try {
+ final List<Station> stations =
+ await railwayStationsRepository.getStations();
+ yield MapLoaded(stations: stations);
+ } catch (_) {
+ yield MapError();
+ }
+ } else if (event is RenderMap) {
+ yield MapRender();
+ }
+ }
+}
diff --git a/lib/components/LoadingIndicator.dart b/lib/components/LoadingIndicator.dart
@@ -0,0 +1,8 @@
+import 'package:flutter/material.dart';
+
+class LoadingIndicator extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) => Center(
+ child: CircularProgressIndicator(),
+ );
+}
diff --git a/lib/components/LoginForm.dart b/lib/components/LoginForm.dart
@@ -0,0 +1,71 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import '../blocs/LoginBloc.dart';
+import '../events/LoginEvent.dart';
+import '../states/LoginState.dart';
+
+class LoginForm extends StatefulWidget {
+ @override
+ State<LoginForm> createState() => _LoginFormState();
+}
+
+class _LoginFormState extends State<LoginForm> {
+ final _usernameController = TextEditingController();
+ final _passwordController = TextEditingController();
+
+ @override
+ Widget build(BuildContext context) {
+ _onLoginButtonPressed() {
+ BlocProvider.of<LoginBloc>(context).add(
+ LoginButtonPressed(
+ username: _usernameController.text,
+ password: _passwordController.text,
+ ),
+ );
+ }
+
+ return BlocListener<LoginBloc, LoginState>(
+ listener: (context, state) {
+ if (state is LoginFailure) {
+ Scaffold.of(context).showSnackBar(
+ SnackBar(
+ content: Text('${state.error}'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ },
+ child: BlocBuilder<LoginBloc, LoginState>(
+ builder: (context, state) {
+ return SafeArea(
+ child: Column(
+ children: [
+ CupertinoTextField(
+ placeholder: 'username',
+ controller: _usernameController,
+ ),
+ CupertinoTextField(
+ placeholder: 'password',
+ controller: _passwordController,
+ obscureText: true,
+ ),
+ RaisedButton(
+ onPressed:
+ state is! LoginLoading ? _onLoginButtonPressed : null,
+ child: Text('Login'),
+ ),
+ Container(
+ child: state is LoginLoading
+ ? CircularProgressIndicator()
+ : null,
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/events/AuthenticationEvent.dart b/lib/events/AuthenticationEvent.dart
@@ -0,0 +1,25 @@
+import 'package:meta/meta.dart';
+import 'package:equatable/equatable.dart';
+
+abstract class AuthenticationEvent extends Equatable {
+ const AuthenticationEvent();
+
+ @override
+ List<Object> get props => [];
+}
+
+class AppStarted extends AuthenticationEvent {}
+
+class LoggedIn extends AuthenticationEvent {
+ final String token;
+
+ const LoggedIn({@required this.token});
+
+ @override
+ List<Object> get props => [token];
+
+ @override
+ String toString() => 'LoggedIn { token: $token }';
+}
+
+class LoggedOut extends AuthenticationEvent {}
diff --git a/lib/events/LoginEvent.dart b/lib/events/LoginEvent.dart
@@ -0,0 +1,23 @@
+import 'package:meta/meta.dart';
+import 'package:equatable/equatable.dart';
+
+abstract class LoginEvent extends Equatable {
+ const LoginEvent();
+}
+
+class LoginButtonPressed extends LoginEvent {
+ final String username;
+ final String password;
+
+ const LoginButtonPressed({
+ @required this.username,
+ @required this.password,
+ });
+
+ @override
+ List<Object> get props => [username, password];
+
+ @override
+ String toString() =>
+ 'LoginButtonPressed { username: $username, password: $password }';
+}
diff --git a/lib/events/MapEvent.dart b/lib/events/MapEvent.dart
@@ -0,0 +1,17 @@
+import 'package:equatable/equatable.dart';
+
+abstract class MapEvent extends Equatable {
+ const MapEvent();
+}
+
+class FetchStations extends MapEvent {
+ const FetchStations();
+
+ @override
+ List<Object> get props => [];
+}
+
+class RenderMap extends MapEvent {
+ @override
+ List<Object> get props => [];
+}
diff --git a/lib/main.dart b/lib/main.dart
@@ -1,18 +1,110 @@
+import 'package:bloc/bloc.dart';
+import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:http/http.dart' as http;
-void main() => runApp(MyApp());
+import 'blocs/AuthenticationBloc.dart';
+import 'blocs/MapBloc.dart';
+import 'components/LoadingIndicator.dart';
+import 'events/AuthenticationEvent.dart';
+import 'repos/RailwayStationsApiClient.dart';
+import 'repos/RailwayStationsRepository.dart';
+import 'repos/UserRepository.dart';
+import 'states/AuthenticationState.dart';
+import 'views/HomePage.dart';
+import 'views/SplashPage.dart';
+
+class SimpleBlocDelegate extends BlocDelegate {
+ @override
+ void onEvent(Bloc bloc, Object event) {
+ super.onEvent(bloc, event);
+ //print(event);
+ }
+
+ @override
+ void onTransition(Bloc bloc, Transition transition) {
+ super.onTransition(bloc, transition);
+ //print(transition);
+ }
+
+ @override
+ void onError(Bloc bloc, Object error, StackTrace stacktrace) {
+ super.onError(bloc, error, stacktrace);
+ //print(error);
+ }
+}
+
+void main() {
+ BlocSupervisor.delegate = SimpleBlocDelegate();
+ final userRepository = UserRepository();
+
+ final RailwayStationsRepository railwayStationsRepository =
+ RailwayStationsRepository(
+ railwayStationsApiClient: RailwayStationsApiClient(
+ httpClient: http.Client(),
+ ),
+ );
+
+ runApp(
+ BlocProvider<AuthenticationBloc>(
+ create: (context) {
+ return AuthenticationBloc(userRepository: userRepository)
+ ..add(AppStarted());
+ },
+ child: App(
+ userRepository: userRepository,
+ railwayStationsRepository: railwayStationsRepository,
+ ),
+ ),
+ );
+}
+
+class App extends StatelessWidget {
+ final UserRepository userRepository;
+ final RailwayStationsRepository railwayStationsRepository;
+
+ App(
+ {Key key,
+ @required this.userRepository,
+ @required this.railwayStationsRepository})
+ : super(key: key);
-class MyApp extends StatelessWidget {
- // This widget is the root of your application.
@override
Widget build(BuildContext context) {
- return MaterialApp(
+ return CupertinoApp(
title: 'Bahnhofsfotos',
- theme: ThemeData(
+ theme: CupertinoThemeData(
+ brightness: Brightness.light,
primaryColor: Color(0xffc71c4d),
- accentColor: Color(0xffD0C332),
+ primaryContrastingColor: Color(0xffD0C332),
+ ),
+ home: BlocBuilder<AuthenticationBloc, AuthenticationState>(
+ builder: (context, state) {
+ if (state is AuthenticationUninitialized) {
+ return SplashPage();
+ }
+ if (state is AuthenticationAuthenticated) {
+ return BlocProvider(
+ create: (context) =>
+ MapBloc(railwayStationsRepository: railwayStationsRepository),
+ child: HomePage(),
+ );
+ }
+ if (state is AuthenticationNeeded) {
+ return BlocProvider(
+ create: (context) =>
+ MapBloc(railwayStationsRepository: railwayStationsRepository),
+ child: HomePage(),
+ );
+ }
+ if (state is AuthenticationLoading) {
+ return LoadingIndicator();
+ } else {
+ return HomePage();
+ }
+ },
),
- home: Container(),
);
}
}
diff --git a/lib/models/countries.dart b/lib/models/countries.dart
@@ -0,0 +1,80 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/cupertino.dart';
+
+class Country extends Equatable {
+ final String code;
+ final String name;
+ final String email;
+ final String twitterTags;
+ final String timetableUrlTemplate;
+ final String overrideLicense;
+ final bool active;
+ final List<ProviderApp> providerApps;
+
+ const Country({
+ @required this.code,
+ @required this.name,
+ @required this.email,
+ @required this.twitterTags,
+ this.timetableUrlTemplate,
+ this.overrideLicense,
+ @required this.active,
+ @required this.providerApps,
+ });
+
+ @override
+ List<Object> get props => [
+ code,
+ name,
+ email,
+ twitterTags,
+ timetableUrlTemplate,
+ overrideLicense,
+ active,
+ providerApps,
+ ];
+
+ static Country fromJson(dynamic json) {
+ List<ProviderApp> providerApps = List();
+ (json["providerApps"] as List).forEach((element) {
+ providerApps.add(ProviderApp.fromJson(element));
+ });
+ return Country(
+ code: json["code"],
+ name: json["name"],
+ email: json["email"],
+ twitterTags: json["twitterTags"],
+ timetableUrlTemplate: json["timetableUrlTemplate"],
+ overrideLicense: json["overrideLicense"],
+ active: json["active"],
+ providerApps: providerApps,
+ );
+ }
+}
+
+class ProviderApp extends Equatable {
+ final String type;
+ final String name;
+ final String url;
+
+ ProviderApp({
+ @required this.type,
+ @required this.name,
+ @required this.url,
+ });
+
+ @override
+ List<Object> get props => [
+ type,
+ name,
+ url,
+ ];
+
+ static ProviderApp fromJson(dynamic json) {
+ return ProviderApp(
+ type: json["type"],
+ name: json["name"],
+ url: json["url"],
+ );
+ }
+}
diff --git a/lib/models/models.dart b/lib/models/models.dart
@@ -0,0 +1,2 @@
+export 'countries.dart';
+export 'stations.dart';
diff --git a/lib/models/stations.dart b/lib/models/stations.dart
@@ -0,0 +1,46 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/cupertino.dart';
+
+class Station extends Equatable {
+ final String country;
+ final String idStr;
+ final int id;
+ final String title;
+ final double lat;
+ final double lon;
+
+ final bool active;
+
+ Station({
+ @required this.country,
+ @required this.idStr,
+ @required this.id,
+ @required this.title,
+ @required this.lat,
+ @required this.lon,
+ @required this.active,
+ });
+
+ @override
+ List<Object> get props => [
+ country,
+ idStr,
+ id,
+ title,
+ lat,
+ lon,
+ active,
+ ];
+
+ static Station fromJson(dynamic json) {
+ return Station(
+ country: json["country"] ?? "",
+ idStr: json["idStr"] ?? "",
+ id: json["id"] ?? null,
+ title: json["title"] ?? "",
+ lat: json["lat"] ?? null,
+ lon: json["lon"] ?? null,
+ active: json["active"] ?? false,
+ );
+ }
+}
diff --git a/lib/repos/RailwayStationsApiClient.dart b/lib/repos/RailwayStationsApiClient.dart
@@ -0,0 +1,46 @@
+import 'dart:convert';
+
+import 'package:flutter/foundation.dart';
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+import '../models/models.dart';
+
+class RailwayStationsApiClient {
+ static const baseUrl = 'https://api.railway-stations.org';
+ final http.Client httpClient;
+
+ RailwayStationsApiClient({
+ @required this.httpClient,
+ }) : assert(httpClient != null);
+
+ Future<List<Country>> getCountries() async {
+ final countriesUrl = '$baseUrl/countries';
+ final countriesResponse = await this.httpClient.get(countriesUrl);
+ if (countriesResponse.statusCode != 200) {
+ throw Exception('error getting countries');
+ }
+
+ List<Country> countries = List();
+ final countriesJson = jsonDecode(countriesResponse.body);
+ (countriesJson as List).forEach((element) {
+ countries.add(Country.fromJson(element));
+ });
+ return countries;
+ }
+
+ Future<List<Station>> getStations() async {
+ final stationsUrl = '$baseUrl/stations';
+ final stationsResponse = await this.httpClient.get(stationsUrl);
+ if (stationsResponse.statusCode != 200) {
+ throw Exception('error getting stations');
+ }
+
+ List<Station> stations = List();
+ final stationsJson = jsonDecode(stationsResponse.body);
+ (stationsJson as List).forEach((element) {
+ stations.add(Station.fromJson(element));
+ });
+ return stations;
+ }
+}
diff --git a/lib/repos/RailwayStationsRepository.dart b/lib/repos/RailwayStationsRepository.dart
@@ -0,0 +1,19 @@
+import 'package:flutter/cupertino.dart';
+
+import '../models/models.dart';
+import 'RailwayStationsApiClient.dart';
+
+class RailwayStationsRepository {
+ final RailwayStationsApiClient railwayStationsApiClient;
+
+ RailwayStationsRepository({@required this.railwayStationsApiClient})
+ : assert(railwayStationsApiClient != null);
+
+ Future<List<Country>> getCountries() async {
+ return await railwayStationsApiClient.getCountries();
+ }
+
+ Future<List<Station>> getStations() async {
+ return await railwayStationsApiClient.getStations();
+ }
+}
diff --git a/lib/repos/UserRepository.dart b/lib/repos/UserRepository.dart
@@ -0,0 +1,29 @@
+import 'package:flutter/cupertino.dart';
+
+class UserRepository {
+ Future<String> authenticate({
+ @required String username,
+ @required String password,
+ }) async {
+ await Future.delayed(Duration(seconds: 1));
+ return 'token';
+ }
+
+ Future<void> deleteToken() async {
+ /// delete from keystore/keychain
+ await Future.delayed(Duration(seconds: 1));
+ return;
+ }
+
+ Future<void> persistToken(String token) async {
+ /// write to keystore/keychain
+ await Future.delayed(Duration(seconds: 1));
+ return;
+ }
+
+ Future<bool> hasToken() async {
+ /// read from keystore/keychain
+ await Future.delayed(Duration(seconds: 1));
+ return false;
+ }
+}
diff --git a/lib/repos/repositories.dart b/lib/repos/repositories.dart
@@ -0,0 +1 @@
+export 'RailwayStationsRepository.dart';
diff --git a/lib/states/AuthenticationState.dart b/lib/states/AuthenticationState.dart
@@ -0,0 +1,14 @@
+import 'package:equatable/equatable.dart';
+
+abstract class AuthenticationState extends Equatable {
+ @override
+ List<Object> get props => [];
+}
+
+class AuthenticationUninitialized extends AuthenticationState {}
+
+class AuthenticationAuthenticated extends AuthenticationState {}
+
+class AuthenticationNeeded extends AuthenticationState {}
+
+class AuthenticationLoading extends AuthenticationState {}
diff --git a/lib/states/LoginState.dart b/lib/states/LoginState.dart
@@ -0,0 +1,25 @@
+import 'package:meta/meta.dart';
+import 'package:equatable/equatable.dart';
+
+abstract class LoginState extends Equatable {
+ const LoginState();
+
+ @override
+ List<Object> get props => [];
+}
+
+class LoginInitial extends LoginState {}
+
+class LoginLoading extends LoginState {}
+
+class LoginFailure extends LoginState {
+ final String error;
+
+ const LoginFailure({@required this.error});
+
+ @override
+ List<Object> get props => [error];
+
+ @override
+ String toString() => 'LoginFailure { error: $error }';
+}
diff --git a/lib/states/MapState.dart b/lib/states/MapState.dart
@@ -0,0 +1,26 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/cupertino.dart';
+
+import '../models/models.dart';
+
+abstract class MapState extends Equatable {
+ const MapState();
+
+ @override
+ List<Object> get props => [];
+}
+
+class MapLoading extends MapState {}
+
+class MapLoaded extends MapState {
+ final List<Station> stations;
+
+ const MapLoaded({@required this.stations}) : assert(stations != null);
+
+ @override
+ List<Object> get props => [stations];
+}
+
+class MapRender extends MapState {}
+
+class MapError extends MapState {}
diff --git a/lib/views/HomePage.dart b/lib/views/HomePage.dart
@@ -0,0 +1,67 @@
+// TODO fix placeholder
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+import 'tabs/MapTab.dart';
+import 'tabs/SettingsTab.dart';
+
+class HomePage extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return CupertinoTabScaffold(
+ tabBar: CupertinoTabBar(
+ items: const <BottomNavigationBarItem>[
+ BottomNavigationBarItem(
+ icon: Icon(CupertinoIcons.location),
+ title: Text('Map'),
+ ),
+ BottomNavigationBarItem(
+ icon: Icon(CupertinoIcons.info),
+ title: Text('Rangliste'),
+ ),
+ BottomNavigationBarItem(
+ icon: Icon(CupertinoIcons.settings),
+ title: Text('Einstellungen'),
+ ),
+ ],
+ ),
+ tabBuilder: (context, index) {
+ switch (index) {
+ case 0:
+ return CupertinoTabView(builder: (context) {
+ return CupertinoPageScaffold(
+ child: MapTab(),
+ );
+ });
+ case 1:
+ return CupertinoTabView(builder: (context) {
+ return CupertinoPageScaffold(
+ child: Container(),
+ );
+ });
+ case 2:
+ return CupertinoTabView(builder: (context) {
+ return CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(
+ middle: Text("Einstellungen"),
+ ),
+ child: SettingsTab(),
+ );
+ });
+ }
+ return CupertinoPageScaffold(
+ child: Container(),
+ );
+ },
+ /* child: Container(
+ child: Center(
+ child: RaisedButton(
+ child: Text('logout'),
+ onPressed: () {
+ BlocProvider.of<AuthenticationBloc>(context).add(LoggedOut());
+ },
+ )),
+ ),*/
+ );
+ }
+}
diff --git a/lib/views/LoginPage.dart b/lib/views/LoginPage.dart
@@ -0,0 +1,36 @@
+// TODO fix placeholder
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+import 'package:flutter_bloc/flutter_bloc.dart';
+import '../repos/UserRepository.dart';
+import '../blocs/LoginBloc.dart';
+import '../blocs/AuthenticationBloc.dart';
+import '../components/LoginForm.dart';
+
+class LoginPage extends StatelessWidget {
+ final UserRepository userRepository;
+
+ LoginPage({Key key, @required this.userRepository})
+ : assert(userRepository != null),
+ super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return CupertinoPageScaffold(
+ navigationBar: CupertinoNavigationBar(
+ middle: const Text('Login'),
+ ),
+ child: BlocProvider(
+ create: (context) {
+ return LoginBloc(
+ authenticationBloc: BlocProvider.of<AuthenticationBloc>(context),
+ userRepository: userRepository,
+ );
+ },
+ child: LoginForm(),
+ ),
+ );
+ }
+}
diff --git a/lib/views/SplashPage.dart b/lib/views/SplashPage.dart
@@ -0,0 +1,14 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class SplashPage extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return CupertinoPageScaffold(
+ child: Center(
+ // TODO fix placeholder
+ child: Text('Splash Screen'),
+ ),
+ );
+ }
+}
diff --git a/lib/views/tabs/MapTab.dart b/lib/views/tabs/MapTab.dart
@@ -0,0 +1,120 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_map/flutter_map.dart';
+import 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart';
+import 'package:latlong/latlong.dart';
+import 'package:rs_flutter_app/events/MapEvent.dart';
+
+import '../../blocs/MapBloc.dart';
+import '../../models/models.dart';
+import '../../states/MapState.dart';
+
+class MapTab extends StatefulWidget {
+ @override
+ MapTabState createState() => MapTabState();
+}
+
+class MapTabState extends State<MapTab> {
+ MapTabState();
+
+ List<Station> stations;
+ final _initialCenter = LatLng(51.133481, 10.018343);
+ final _initialZoom = 6.0;
+
+ List<Marker> _makeStationMarkers(List<Station> stations) {
+ List<Marker> stationMarkers = List();
+ stations.forEach((element) {
+ stationMarkers.add(Marker(
+ anchorPos: AnchorPos.align(AnchorAlign.center),
+ height: 30,
+ width: 30,
+ point: LatLng(element.lat, element.lon),
+ builder: (BuildContext context) => Icon(Icons.pin_drop),
+ ));
+ });
+ return stationMarkers;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // Fetch map data on opening
+ if (stations == null) {
+ BlocProvider.of<MapBloc>(context).add(FetchStations());
+ }
+
+ return BlocBuilder<MapBloc, MapState>(builder: (context, state) {
+ if (state is MapLoading) {
+ return Center(child: CircularProgressIndicator());
+ }
+ if (state is MapRender) {
+ return FlutterMap(
+ options: MapOptions(
+ center: _initialCenter,
+ zoom: _initialZoom,
+ plugins: [
+ MarkerClusterPlugin(),
+ ],
+ ),
+ layers: [
+ TileLayerOptions(
+ urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
+ subdomains: ['a', 'b', 'c'],
+ ),
+ MarkerClusterLayerOptions(
+ maxClusterRadius: 120,
+ size: Size(50, 50),
+ fitBoundsOptions: FitBoundsOptions(
+ padding: EdgeInsets.all(50),
+ ),
+ markers: _makeStationMarkers(stations),
+ polygonOptions: PolygonOptions(
+ borderColor: Colors.blueAccent,
+ color: Colors.black12,
+ borderStrokeWidth: 3,
+ ),
+ builder: (context, markers) {
+ return ClipRRect(
+ borderRadius: BorderRadius.circular(50),
+ child: Container(
+ color: CupertinoTheme.of(context).primaryColor,
+ child: Center(
+ child: Text(
+ markers.length.toString(),
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ],
+ );
+ }
+ if (state is MapLoaded) {
+ return FutureBuilder(
+ future: _cacheData(state.stations),
+ builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
+ return Center(child: CircularProgressIndicator());
+ },
+ );
+ }
+ if (state is MapError) {
+ return Text(
+ 'Something went wrong!',
+ style: TextStyle(color: Colors.red),
+ );
+ }
+ return Text(
+ 'Something went wrong2!',
+ style: TextStyle(color: Colors.red),
+ );
+ });
+ }
+
+ _cacheData(List<Station> stations) async {
+ setState(() {
+ this.stations = stations;
+ BlocProvider.of<MapBloc>(context).add(RenderMap());
+ });
+ }
+}
diff --git a/lib/views/tabs/SettingsTab.dart b/lib/views/tabs/SettingsTab.dart
@@ -0,0 +1,36 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:settings_ui/settings_ui.dart';
+
+class SettingsTab extends StatefulWidget {
+ @override
+ SettingsTabState createState() => SettingsTabState();
+}
+
+class SettingsTabState extends State<SettingsTab> {
+ @override
+ Widget build(BuildContext context) {
+ return Material(
+ child: SettingsList(sections: [
+ SettingsSection(
+ title: 'Bahnhofsdaten',
+ tiles: [
+ SettingsTile(
+ title: 'Länderdaten aktualisieren',
+ leading: Icon(Icons.language),
+ onTap: () {},
+ ),
+ ],
+ ),
+ SettingsSection(
+ title: 'Lizensierung',
+ tiles: [],
+ ),
+ SettingsSection(
+ title: 'Verlinkung',
+ tiles: [],
+ ),
+ ]),
+ );
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
@@ -15,6 +15,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.39.4"
+ ansicolor:
+ dependency: transitive
+ description:
+ name: ansicolor
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.2"
archive:
dependency: transitive
description:
@@ -42,7 +49,7 @@ packages:
name: bloc
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.0"
+ version: "3.0.0"
boolean_selector:
dependency: transitive
description:
@@ -50,6 +57,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
+ cached_network_image:
+ dependency: transitive
+ description:
+ name: cached_network_image
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
charcode:
dependency: transitive
description:
@@ -64,6 +78,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.11"
+ console_log_handler:
+ dependency: transitive
+ description:
+ name: console_log_handler
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.6"
convert:
dependency: transitive
description:
@@ -99,6 +120,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
+ equatable:
+ dependency: "direct main"
+ description:
+ name: equatable
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.6.1"
file:
dependency: transitive
description:
@@ -117,12 +145,40 @@ packages:
name: flutter_bloc
url: "https://pub.dartlang.org"
source: hosted
- version: "2.1.1"
+ version: "3.2.0"
+ flutter_cache_manager:
+ dependency: transitive
+ description:
+ name: flutter_cache_manager
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.3"
flutter_driver:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
+ flutter_image:
+ dependency: transitive
+ description:
+ name: flutter_image
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.0"
+ flutter_map:
+ dependency: "direct main"
+ description:
+ name: flutter_map
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.8.2"
+ flutter_map_marker_cluster:
+ dependency: "direct main"
+ description:
+ name: flutter_map_marker_cluster
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.2.7"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -203,6 +259,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
+ latlong:
+ dependency: "direct main"
+ description:
+ name: latlong
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.6.1"
logging:
dependency: transitive
description:
@@ -238,6 +301,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
+ nested:
+ dependency: transitive
+ description:
+ name: nested
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.0.4"
node_interop:
dependency: transitive
description:
@@ -280,6 +350,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.4"
+ path_provider:
+ dependency: transitive
+ description:
+ name: path_provider
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.6.1"
pedantic:
dependency: transitive
description:
@@ -308,6 +385,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
+ positioned_tap_detector:
+ dependency: transitive
+ description:
+ name: positioned_tap_detector
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.3"
process:
dependency: transitive
description:
@@ -321,7 +405,7 @@ packages:
name: provider
url: "https://pub.dartlang.org"
source: hosted
- version: "3.2.0"
+ version: "4.0.4"
pub_semver:
dependency: transitive
description:
@@ -342,7 +426,14 @@ packages:
name: rxdart
url: "https://pub.dartlang.org"
source: hosted
- version: "0.22.6"
+ version: "0.23.1"
+ settings_ui:
+ dependency: "direct main"
+ description:
+ name: settings_ui
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.2.0"
shelf:
dependency: transitive
description:
@@ -397,6 +488,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.5"
+ sqflite:
+ dependency: transitive
+ description:
+ name: sqflite
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.1"
stack_trace:
dependency: transitive
description:
@@ -425,6 +523,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
+ synchronized:
+ dependency: transitive
+ description:
+ name: synchronized
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.2.0"
term_glyph:
dependency: transitive
description:
@@ -453,6 +558,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.15"
+ transparent_image:
+ dependency: transitive
+ description:
+ name: transparent_image
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
+ tuple:
+ dependency: transitive
+ description:
+ name: tuple
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.3"
typed_data:
dependency: transitive
description:
@@ -460,6 +579,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
+ uuid:
+ dependency: transitive
+ description:
+ name: uuid
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.4"
+ validate:
+ dependency: transitive
+ description:
+ name: validate
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.7.0"
vector_math:
dependency: transitive
description:
@@ -518,3 +651,4 @@ packages:
version: "2.2.0"
sdks:
dart: ">=2.6.0 <3.0.0"
+ flutter: ">=1.12.1 <2.0.0"
diff --git a/pubspec.yaml b/pubspec.yaml
@@ -22,12 +22,18 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
- cupertino_icons: ^0.1.2
+ cupertino_icons: ^0.1.3
- bloc: ^2.0.0
- flutter_bloc: ^2.0.0
+ bloc: ^3.0.0
+ flutter_bloc: ^3.2.0
equatable: ^0.6.0
+ flutter_map: ^0.8.2
+ latlong: ^0.6.1
+ flutter_map_marker_cluster: ^0.2.7
+
+ settings_ui: ^0.2.0
+
dev_dependencies:
flutter_test:
sdk: flutter
diff --git a/test/widget_test.dart b/test/widget_test.dart
@@ -1,13 +1 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility that Flutter provides. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import 'package:rs_flutter_app/main.dart';
-
void main() {}
diff --git a/test_driver/app_test.dart b/test_driver/app_test.dart
@@ -5,7 +5,7 @@ import 'package:test/test.dart';
void main() {
group('Bahnhofsfotos App', () {
FlutterDriver driver;
- final String backButtonToolTip = "Back"; // Set to your preferred language
+ //final String backButtonToolTip = "Back"; // Set to your preferred language
// Connect to the Flutter driver before running any tests
setUpAll(() async {
@@ -19,7 +19,7 @@ void main() {
}
});
- var waitFor = (String key) async {
+ /*var waitFor = (String key) async {
print("[Integration Test] Wait for $key");
await driver.waitFor(find.byValueKey(key));
};
@@ -43,6 +43,6 @@ void main() {
var goBack = () async {
await driver.waitFor(find.byTooltip(backButtonToolTip));
await driver.tap(find.byTooltip(backButtonToolTip));
- };
+ };*/
});
}