rsflutterapp

Bahnhofsfoto-App for Android and iOS using Flutter
git clone git://archive.git.mtrnord.blog/MTRNord/rsflutterapp.git
Log | Files | Refs | README

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:
Mandroid/app/build.gradle | 2+-
Mandroid/app/src/main/AndroidManifest.xml | 1+
Alib/blocs/AuthenticationBloc.dart | 44++++++++++++++++++++++++++++++++++++++++++++
Alib/blocs/LoginBloc.dart | 42++++++++++++++++++++++++++++++++++++++++++
Alib/blocs/MapBloc.dart | 33+++++++++++++++++++++++++++++++++
Alib/components/LoadingIndicator.dart | 8++++++++
Alib/components/LoginForm.dart | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/events/AuthenticationEvent.dart | 25+++++++++++++++++++++++++
Alib/events/LoginEvent.dart | 23+++++++++++++++++++++++
Alib/events/MapEvent.dart | 17+++++++++++++++++
Mlib/main.dart | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Alib/models/countries.dart | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/models/models.dart | 2++
Alib/models/stations.dart | 46++++++++++++++++++++++++++++++++++++++++++++++
Alib/repos/RailwayStationsApiClient.dart | 46++++++++++++++++++++++++++++++++++++++++++++++
Alib/repos/RailwayStationsRepository.dart | 19+++++++++++++++++++
Alib/repos/UserRepository.dart | 29+++++++++++++++++++++++++++++
Alib/repos/repositories.dart | 1+
Alib/states/AuthenticationState.dart | 14++++++++++++++
Alib/states/LoginState.dart | 25+++++++++++++++++++++++++
Alib/states/MapState.dart | 26++++++++++++++++++++++++++
Alib/views/HomePage.dart | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/views/LoginPage.dart | 36++++++++++++++++++++++++++++++++++++
Alib/views/SplashPage.dart | 14++++++++++++++
Alib/views/tabs/MapTab.dart | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/views/tabs/SettingsTab.dart | 36++++++++++++++++++++++++++++++++++++
Mpubspec.lock | 142++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mpubspec.yaml | 12+++++++++---
Mtest/widget_test.dart | 12------------
Mtest_driver/app_test.dart | 6+++---
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)); - }; + };*/ }); }