diff --git a/README.md b/README.md index 51aec24..822d204 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,15 @@ # Racego -A cross-platform race-management tool. +A cross-platform race-management tool for Desktop and web. +

+ +

+

Home screen with dark an light theme: quick access to user-management and ranking list

+ +## Languages + +English and German languages are supported. ## Supported Platforms @@ -18,8 +26,19 @@ Every idea is welcome. Feel free to contribute via pull request or opening a iss ## Some backgrounds about Racego -Racego was first released as a native Windows application in 2020. Later on, Neofix further increased the capabilities and refactored everythin up to a new release in 2021. Racego did served well on it's first tests and as it grows in popularity, Neofix decided to bring Racego to cross-platform with further possibilities of extensions. +Racego was first released as a native Windows application in 2020. Later on, Neofix further increased the capabilities and refactored everything up to a new release in 2021. Racego did served well on it's first tests and as it grows in popularity, Neofix decided to bring Racego to cross-platform with further possibilities of extensions. ## License & copyright - + Licensed under the [GPLv3 License](LICENSE). + +## Screenshots + +

Edit profile of every participant

+

+ +

+ +

+ +

diff --git a/lib/business_logic/login/login_bloc.dart b/lib/business_logic/login/login_bloc.dart index fc9a497..48fb6f3 100644 --- a/lib/business_logic/login/login_bloc.dart +++ b/lib/business_logic/login/login_bloc.dart @@ -2,6 +2,7 @@ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:racego/data/api/racego_api.dart'; import 'package:racego/data/exceptions/racego_exception.dart'; +import 'package:racego/generated/l10n.dart'; part 'login_event.dart'; part 'login_state.dart'; @@ -24,7 +25,7 @@ class LoginBloc extends Bloc { emit(LoggedOut()); } } on RacegoException catch (_) { - emit(LoginError('Melden Sie sich erneut an')); + emit(LoginError(S.current.retry_login)); } } @@ -35,7 +36,7 @@ class LoginBloc extends Bloc { if (isLoggedIn) { emit(LoggedIn(username: _api.username)); } else { - emit(LoginError('Email oder Passwort ist ungültig')); + emit(LoginError(S.current.login_invalid)); } } on RacegoException catch (racegoException) { emit(LoginError(racegoException.errorMessage)); diff --git a/lib/business_logic/ranking_cubit/ranking_cubit.dart b/lib/business_logic/ranking_cubit/ranking_cubit.dart new file mode 100644 index 0000000..fff7660 --- /dev/null +++ b/lib/business_logic/ranking_cubit/ranking_cubit.dart @@ -0,0 +1,60 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:racego/data/models/rankinglist.dart'; + +import '../../data/api/racego_api.dart'; +import '../../data/exceptions/racego_exception.dart'; +import '../../generated/l10n.dart'; + +part 'rankingcubit_state.dart'; + +class RankingCubit extends Cubit { + RankingCubit(RacegoApi api) + : _api = api, + super(const RankingLoading(currentClass: '', classList: [])) { + loadClasses(); + } + final RacegoApi _api; + + List _classes = []; + String _currentClass = ''; + RankingList _currentRanking = RankingList(null); + + Future loadClasses() async { + try { + _classes = await _api.getCategories(); + // if (!_classes.contains(_currentClass)) { + // _currentClass = ''; + // _classes.clear(); + // } + emit(RankingLoaded(_currentRanking, + currentClass: _currentClass, classList: _classes)); + } catch (e) { + if (e is RacegoException) { + emit(RankingError(e, currentClass: _currentClass, classList: _classes)); + } else { + UnknownException error = UnknownException( + S.current.unknown_error, e.toString(), e.runtimeType.toString()); + emit(RankingError(error, + currentClass: _currentClass, classList: _classes)); + } + } + } + + Future loadRanking(String? raceClass) async { + try { + _currentRanking = await _api.getRankig(raceClass); + emit(RankingLoaded(_currentRanking, + currentClass: _currentClass, classList: _classes)); + } catch (e) { + if (e is RacegoException) { + emit(RankingError(e, currentClass: _currentClass, classList: _classes)); + } else { + UnknownException error = UnknownException( + S.current.unknown_error, e.toString(), e.runtimeType.toString()); + emit(RankingError(error, + currentClass: _currentClass, classList: _classes)); + } + } + } +} diff --git a/lib/business_logic/ranking_cubit/rankingcubit_state.dart b/lib/business_logic/ranking_cubit/rankingcubit_state.dart new file mode 100644 index 0000000..9f2d959 --- /dev/null +++ b/lib/business_logic/ranking_cubit/rankingcubit_state.dart @@ -0,0 +1,29 @@ +part of 'ranking_cubit.dart'; + +@immutable +abstract class RankingcubitState { + const RankingcubitState( + {required this.currentClass, required this.classList}); + final String currentClass; + final List classList; +} + +class RankingLoading extends RankingcubitState { + const RankingLoading( + {required String currentClass, required List classList}) + : super(currentClass: currentClass, classList: classList); +} + +class RankingLoaded extends RankingcubitState { + const RankingLoaded(this.ranking, + {required String currentClass, required List classList}) + : super(currentClass: currentClass, classList: classList); + final RankingList ranking; +} + +class RankingError extends RankingcubitState { + const RankingError(this.exception, + {required String currentClass, required List classList}) + : super(currentClass: currentClass, classList: classList); + final RacegoException exception; +} diff --git a/lib/business_logic/tracklist_cubit/tracklist_cubit.dart b/lib/business_logic/tracklist_cubit/tracklist_cubit.dart index 8155bae..f4c1754 100644 --- a/lib/business_logic/tracklist_cubit/tracklist_cubit.dart +++ b/lib/business_logic/tracklist_cubit/tracklist_cubit.dart @@ -5,6 +5,8 @@ import 'package:racego/data/exceptions/racego_exception.dart'; import 'package:racego/data/models/time.dart'; import 'package:racego/data/models/user.dart'; +import '../../generated/l10n.dart'; + part 'tracklist_state.dart'; class TracklistCubit extends Cubit { @@ -52,7 +54,7 @@ class TracklistCubit extends Cubit { )); } else { UnknownException error = UnknownException( - 'Unbekannter Fehler', e.toString(), e.runtimeType.toString()); + S.current.unknown_error, e.toString(), e.runtimeType.toString()); emit(Error( error, _filter.isNotEmpty ? _filterList(_newestList, _filter) : _newestList, @@ -81,8 +83,7 @@ class TracklistCubit extends Cubit { try { bool successful = await _api.cancelLap(userId); if (!successful) { - throw DataException( - 'Benutzer konnte nicht von der Rennstrecke entfernt werden: Id ungültig.'); + throw DataException(S.current.failed_cancelling_lap_invalid_id); } reload(); } catch (e) { @@ -94,7 +95,7 @@ class TracklistCubit extends Cubit { : _newestList)); } else { UnknownException error = UnknownException( - 'Unbekannter Fehler', e.toString(), e.runtimeType.toString()); + S.current.unknown_error, e.toString(), e.runtimeType.toString()); emit(Error( error, _filter.isNotEmpty @@ -110,8 +111,7 @@ class TracklistCubit extends Cubit { try { bool successful = await _api.finishLap(userId, time); if (!successful) { - throw DataException( - 'Rundenzeit konnte nicht erfasst werden: Id oder Zeit ungültig.'); + throw DataException(S.current.failed_finishing_lap_invalid_id); } reload(); } catch (e) { @@ -123,7 +123,7 @@ class TracklistCubit extends Cubit { : _newestList)); } else { UnknownException error = UnknownException( - 'Unbekannter Fehler', e.toString(), e.runtimeType.toString()); + S.current.unknown_error, e.toString(), e.runtimeType.toString()); emit(Error( error, _filter.isNotEmpty diff --git a/lib/business_logic/tracklist_cubit/tracklist_state.dart b/lib/business_logic/tracklist_cubit/tracklist_state.dart index 6b27e98..f32d604 100644 --- a/lib/business_logic/tracklist_cubit/tracklist_state.dart +++ b/lib/business_logic/tracklist_cubit/tracklist_state.dart @@ -1,14 +1,3 @@ -// part of 'tracklist_cubit.dart'; - -// @immutable -// abstract class TracklistState {} - -// class Loading extends TracklistState {} - -// class Loaded extends TracklistState {} - -// class Error extends TracklistState {} - part of 'tracklist_cubit.dart'; @immutable diff --git a/lib/business_logic/userlist_cubit/userlist_cubit.dart b/lib/business_logic/userlist_cubit/userlist_cubit.dart index c8e69a3..59dac27 100644 --- a/lib/business_logic/userlist_cubit/userlist_cubit.dart +++ b/lib/business_logic/userlist_cubit/userlist_cubit.dart @@ -4,6 +4,8 @@ import 'package:racego/data/api/racego_api.dart'; import 'package:racego/data/exceptions/racego_exception.dart'; import 'package:racego/data/models/user.dart'; +import '../../generated/l10n.dart'; + part 'userlist_state.dart'; class UserlistCubit extends Cubit { @@ -51,7 +53,7 @@ class UserlistCubit extends Cubit { )); } else { UnknownException error = UnknownException( - 'Unbekannter Fehler', e.toString(), e.runtimeType.toString()); + S.current.unknown_error, e.toString(), e.runtimeType.toString()); emit(Error( error, _filter.isNotEmpty ? _filterList(_newestList, _filter) : _newestList, @@ -67,8 +69,7 @@ class UserlistCubit extends Cubit { try { bool successful = await _api.deleteUser(userId); if (!successful) { - throw DataException( - 'Benutzer konnte nicht entfernt werden: Id ungültig.'); + throw DataException(S.current.failed_removing_user_invalid_id); } reload(); } catch (e) { @@ -80,7 +81,7 @@ class UserlistCubit extends Cubit { : _newestList)); } else { UnknownException error = UnknownException( - 'Unbekannter Fehler', e.toString(), e.runtimeType.toString()); + S.current.unknown_error, e.toString(), e.runtimeType.toString()); emit(Error( error, _filter.isNotEmpty @@ -96,8 +97,7 @@ class UserlistCubit extends Cubit { try { bool successful = await _api.addOnTrack(userId); if (!successful) { - throw DataException( - 'Benutzer konnte nicht auf die Rennstrecke gestellt werden: Id ungültig.'); + throw DataException(S.current.failed_adding_user_on_track_invalid_id); } reload(); } catch (e) { @@ -109,7 +109,7 @@ class UserlistCubit extends Cubit { : _newestList)); } else { UnknownException error = UnknownException( - 'Unbekannter Fehler', e.toString(), e.runtimeType.toString()); + S.current.unknown_error, e.toString(), e.runtimeType.toString()); emit(Error( error, _filter.isNotEmpty diff --git a/lib/business_logic/userscreen_cubit/userscreen_cubit.dart b/lib/business_logic/userscreen_cubit/userscreen_cubit.dart index fdf69c6..175e0a3 100644 --- a/lib/business_logic/userscreen_cubit/userscreen_cubit.dart +++ b/lib/business_logic/userscreen_cubit/userscreen_cubit.dart @@ -4,6 +4,8 @@ import 'package:racego/data/api/racego_api.dart'; import 'package:racego/data/exceptions/racego_exception.dart'; import 'package:racego/data/models/userdetails.dart'; +import '../../generated/l10n.dart'; + part 'userscreen_state.dart'; class UserscreenCubit extends Cubit { @@ -27,14 +29,13 @@ class UserscreenCubit extends Cubit { if (id > 0) { loadEditUser(id); } else { - throw UnknownException( - 'Benutzer konnte nicht erstellt werden: Datenbank Id ist ungültig'); + throw UnknownException(S.current.failed_adding_user_invalid_id); } } on RacegoException catch (e) { emit(UserScreenAddError(e)); } catch (error) { - UnknownException e = UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + UnknownException e = UnknownException(S.current.unknown_error, + error.toString(), error.runtimeType.toString()); emit(UserScreenAddError(e)); } } @@ -51,8 +52,8 @@ class UserscreenCubit extends Cubit { } on RacegoException catch (e) { emit(UserScreenEditError(e)); } catch (error) { - UnknownException e = UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + UnknownException e = UnknownException(S.current.unknown_error, + error.toString(), error.runtimeType.toString()); emit(UserScreenEditError(e)); } } @@ -68,13 +69,13 @@ class UserscreenCubit extends Cubit { emit(UserScreenStored()); } else { throw UnknownException( - 'Benutzer konnte nicht aktualisiert werden: Unerwartete Serverantwort'); + S.current.failed_updating_user_unexpected_response); } } on RacegoException catch (e) { emit(UserScreenEditError(e)); } catch (error) { - UnknownException e = UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + UnknownException e = UnknownException(S.current.unknown_error, + error.toString(), error.runtimeType.toString()); emit(UserScreenEditError(e)); } } diff --git a/lib/data/api/racego_api.dart b/lib/data/api/racego_api.dart index a98ac46..a967e2c 100644 --- a/lib/data/api/racego_api.dart +++ b/lib/data/api/racego_api.dart @@ -5,10 +5,13 @@ import 'package:http/http.dart' as http; import 'package:racego/data/exceptions/racego_exception.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:racego/data/models/rankinglist.dart'; import 'package:racego/data/models/time.dart'; import 'package:racego/data/models/user.dart'; import 'package:racego/data/models/userdetails.dart'; +import '../../generated/l10n.dart'; + class RacegoApi { String? _username; bool _isLoggedIn = false; @@ -43,12 +46,12 @@ class RacegoApi { } on RacegoException catch (_) { rethrow; } on TypeError catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } on FormatException catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } catch (error) { - throw UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + throw UnknownException(S.current.unknown_error, error.toString(), + error.runtimeType.toString()); } } @@ -67,7 +70,7 @@ class RacegoApi { } return false; } on AuthException catch (authError) { - if (authError.errorMessage.contains('Login fehlgeschlagen')) { + if (authError.errorMessage.contains(S.current.failed_login)) { return false; } else { rethrow; @@ -75,12 +78,12 @@ class RacegoApi { } on RacegoException catch (_) { rethrow; } on TypeError catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } on FormatException catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } catch (error) { - throw UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + throw UnknownException(S.current.unknown_error, error.toString(), + error.runtimeType.toString()); } } @@ -99,12 +102,12 @@ class RacegoApi { } on RacegoException catch (_) { rethrow; } on TypeError catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } on FormatException catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } catch (error) { - throw UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + throw UnknownException(S.current.unknown_error, error.toString(), + error.runtimeType.toString()); } } @@ -119,12 +122,12 @@ class RacegoApi { } on RacegoException catch (_) { rethrow; } on TypeError catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } on FormatException catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } catch (error) { - throw UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + throw UnknownException(S.current.unknown_error, error.toString(), + error.runtimeType.toString()); } } @@ -139,12 +142,12 @@ class RacegoApi { } on RacegoException catch (_) { rethrow; } on TypeError catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } on FormatException catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } catch (error) { throw UnknownException( - 'Unbekannter Fehler', + S.current.unknown_error, error.toString(), error.runtimeType.toString(), ); @@ -161,19 +164,19 @@ class RacegoApi { } on RacegoException catch (_) { rethrow; } on TypeError catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } on FormatException catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } catch (error) { - throw UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + throw UnknownException(S.current.unknown_error, error.toString(), + error.runtimeType.toString()); } } Future setUserDetails(UserDetails user) async { try { if (user.id <= 0 || user.firstName.isEmpty || user.lastName.isEmpty) { - throw DataException('Die Benutzerangaben sind ungenügend'); + throw DataException(S.current.failed_updating_user_invalid_data); } String response = await _putRequest( @@ -189,19 +192,19 @@ class RacegoApi { } on RacegoException catch (_) { rethrow; } on TypeError catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } on FormatException catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } catch (error) { - throw UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + throw UnknownException(S.current.unknown_error, error.toString(), + error.runtimeType.toString()); } } Future addUser(UserDetails user) async { try { if (user.firstName.isEmpty || user.lastName.isEmpty) { - throw DataException('Die Benutzerangaben sind ungenügend'); + throw DataException(S.current.failed_updating_user_invalid_data); } String response = await _postRequest( @@ -216,12 +219,12 @@ class RacegoApi { } on RacegoException catch (_) { rethrow; } on TypeError catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } on FormatException catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } catch (error) { - throw UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + throw UnknownException(S.current.unknown_error, error.toString(), + error.runtimeType.toString()); } } @@ -242,12 +245,12 @@ class RacegoApi { } on RacegoException catch (_) { rethrow; } on TypeError catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } on FormatException catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } catch (error) { throw UnknownException( - 'Unbekannter Fehler', + S.current.unknown_error, error.toString(), error.runtimeType.toString(), ); @@ -270,12 +273,12 @@ class RacegoApi { } on RacegoException catch (_) { rethrow; } on TypeError catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } on FormatException catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } catch (error) { throw UnknownException( - 'Unbekannter Fehler', + S.current.unknown_error, error.toString(), error.runtimeType.toString(), ); @@ -298,12 +301,12 @@ class RacegoApi { } on RacegoException catch (_) { rethrow; } on TypeError catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } on FormatException catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } catch (error) { throw UnknownException( - 'Unbekannter Fehler', + S.current.unknown_error, error.toString(), error.runtimeType.toString(), ); @@ -327,12 +330,12 @@ class RacegoApi { } on RacegoException catch (_) { rethrow; } on TypeError catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } on FormatException catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } catch (error) { throw UnknownException( - 'Unbekannter Fehler', + S.current.unknown_error, error.toString(), error.runtimeType.toString(), ); @@ -349,12 +352,38 @@ class RacegoApi { } on RacegoException catch (_) { rethrow; } on TypeError catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } on FormatException catch (_) { - throw DataException('Fehler beim Parsen der Serverantwort'); + throw DataException(S.current.failed_parsing_response); } catch (error) { - throw UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + throw UnknownException(S.current.unknown_error, error.toString(), + error.runtimeType.toString()); + } + } + + Future getRankig(String? className) async { + try { + String response = ''; + if (className != null && className.isNotEmpty) { + // escape space to generate api url + className.replaceAll(' ', '%'); + response = await _getRequest(_apiBaseUrl + 'v1/ranking/' + className); + } else { + response = await _getRequest(_apiBaseUrl + 'v1/ranking/all'); + } + List ranks = jsonDecode(response); + return RankingList.fromJson(ranks); + } on AuthException catch (_) { + rethrow; + } on RacegoException catch (_) { + rethrow; + } on TypeError catch (_) { + throw DataException(S.current.failed_parsing_response); + } on FormatException catch (_) { + throw DataException(S.current.failed_parsing_response); + } catch (error) { + throw UnknownException(S.current.unknown_error, error.toString(), + error.runtimeType.toString()); } } @@ -370,32 +399,30 @@ class RacegoApi { case 401: _isLoggedIn = false; _username = null; - throw AuthException('Keine Berechtigung'); + throw AuthException(S.current.no_permission); case 403: _isLoggedIn = false; _username = null; - throw AuthException('Login Fehlgeschlagen'); + throw AuthException(S.current.failed_login); case 404: - throw ServerException( - 'Fehler: Fehlerhafte Quelle für den Datenaustausch.'); + throw ServerException(S.current.requestet_entity_not_found); case 409: - throw DataException('Konflikt bei den gesendeten Daten'); + throw DataException(S.current.failed_send_conflicting_data); case 422: - throw DataException('Eingabe konnte nicht verarbeitet werden'); + throw DataException(S.current.unprocessable_entity); default: throw ServerException( - 'Serverantwort ungültig. Statuscode ${response.statusCode}'); + S.current.invalid_server_response(response.statusCode)); } } on RacegoException catch (_) { rethrow; } on SocketException catch (_) { - throw InternetException('Der Server kann nicht erreicht werden.'); + throw InternetException(S.current.failed_server_timeout); } on TimeoutException catch (_) { - throw InternetException( - 'Der Server kann nicht erreicht werden: Timeout.'); + throw InternetException(S.current.failed_server_timeout); } catch (error) { - throw UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + throw UnknownException(S.current.unknown_error, error.toString(), + error.runtimeType.toString()); } } @@ -411,32 +438,30 @@ class RacegoApi { case 401: _isLoggedIn = false; _username = null; - throw AuthException('Keine Berechtigung'); + throw AuthException(S.current.no_permission); case 403: _isLoggedIn = false; _username = null; - throw AuthException('Login fehlgeschlagen'); + throw AuthException(S.current.failed_login); case 404: - throw ServerException( - 'Fehler: Fehlerhafte Quelle für den Datenaustausch.'); + throw ServerException(S.current.requestet_entity_not_found); case 409: - throw DataException('Konflikt bei den gesendeten Daten'); + throw DataException(S.current.failed_send_conflicting_data); case 422: - throw DataException('Eingabe konnte nicht verarbeitet werden'); + throw DataException(S.current.unprocessable_entity); default: throw ServerException( - 'Serverantwort ungültig. Statuscode ${response.statusCode}'); + S.current.invalid_server_response(response.statusCode)); } } on RacegoException catch (_) { rethrow; } on SocketException catch (_) { - throw InternetException('Der Server kann nicht erreicht werden.'); + throw InternetException(S.current.failed_server_timeout); } on TimeoutException catch (_) { - throw InternetException( - 'Der Server kann nicht erreicht werden: Timeout.'); + throw InternetException(S.current.failed_server_timeout); } catch (error) { - throw UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + throw UnknownException(S.current.unknown_error, error.toString(), + error.runtimeType.toString()); } } @@ -452,32 +477,30 @@ class RacegoApi { case 401: _isLoggedIn = false; _username = null; - throw AuthException('Keine Berechtigung'); + throw AuthException(S.current.no_permission); case 403: _isLoggedIn = false; _username = null; - throw AuthException('Login fehlgeschlagen'); + throw AuthException(S.current.failed_login); case 404: - throw ServerException( - 'Fehler: Fehlerhafte Quelle für den Datenaustausch.'); + throw ServerException(S.current.requestet_entity_not_found); case 409: - throw DataException('Konflikt bei den gesendeten Daten'); + throw DataException(S.current.failed_send_conflicting_data); case 422: - throw DataException('Eingabe konnte nicht verarbeitet werden'); + throw DataException(S.current.unprocessable_entity); default: throw ServerException( - 'Serverantwort ungültig. Statuscode ${response.statusCode}'); + S.current.invalid_server_response(response.statusCode)); } } on RacegoException catch (_) { rethrow; } on SocketException catch (_) { - throw InternetException('Der Server kann nicht erreicht werden.'); + throw InternetException(S.current.failed_server_timeout); } on TimeoutException catch (_) { - throw InternetException( - 'Der Server kann nicht erreicht werden: Timeout.'); + throw InternetException(S.current.failed_server_timeout); } catch (error) { - throw UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + throw UnknownException(S.current.unknown_error, error.toString(), + error.runtimeType.toString()); } } @@ -493,32 +516,30 @@ class RacegoApi { case 401: _isLoggedIn = false; _username = null; - throw AuthException('Keine Berechtigung'); + throw AuthException(S.current.no_permission); case 403: _isLoggedIn = false; _username = null; - throw AuthException('Login fehlgeschlagen'); + throw AuthException(S.current.failed_login); case 404: - throw ServerException( - 'Fehler: Fehlerhafte Quelle für den Datenaustausch.'); + throw ServerException(S.current.requestet_entity_not_found); case 409: - throw DataException('Konflikt bei den gesendeten Daten'); + throw DataException(S.current.failed_send_conflicting_data); case 422: - throw DataException('Eingabe konnte nicht verarbeitet werden'); + throw DataException(S.current.unprocessable_entity); default: throw ServerException( - 'Serverantwort ungültig. Statuscode ${response.statusCode}'); + S.current.invalid_server_response(response.statusCode)); } } on RacegoException catch (_) { rethrow; } on SocketException catch (_) { - throw InternetException('Der Server kann nicht erreicht werden.'); + throw InternetException(S.current.failed_server_timeout); } on TimeoutException catch (_) { - throw InternetException( - 'Der Server kann nicht erreicht werden: Timeout.'); + throw InternetException(S.current.failed_server_timeout); } catch (error) { - throw UnknownException( - 'Unbekannter Fehler', error.toString(), error.runtimeType.toString()); + throw UnknownException(S.current.unknown_error, error.toString(), + error.runtimeType.toString()); } } diff --git a/lib/data/models/rankinglist.dart b/lib/data/models/rankinglist.dart new file mode 100644 index 0000000..e055335 --- /dev/null +++ b/lib/data/models/rankinglist.dart @@ -0,0 +1,22 @@ +class RankingList { + RankingList(List>? ranks) : _ranks = ranks ?? []; + + final List> _ranks; + + Map getValue(int index) { + if (index >= _ranks.length || index < 0) { + return {'dsf': 'sdf'}; + } else { + return _ranks[index]; + } + } + + int get length => _ranks.length; + + static RankingList fromJson(List json) { + List> ranking = + json.map((e) => {e['name'].toString(): e['time'].toString()}).toList(); + + return RankingList(ranking); + } +} diff --git a/lib/generated/intl/messages_all.dart b/lib/generated/intl/messages_all.dart new file mode 100644 index 0000000..ee942cb --- /dev/null +++ b/lib/generated/intl/messages_all.dart @@ -0,0 +1,66 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that looks up messages for specific locales by +// delegating to the appropriate library. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:implementation_imports, file_names, unnecessary_new +// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering +// ignore_for_file:argument_type_not_assignable, invalid_assignment +// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases +// ignore_for_file:comment_references + +import 'dart:async'; + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; +import 'package:intl/src/intl_helpers.dart'; + +import 'messages_de.dart' as messages_de; +import 'messages_en.dart' as messages_en; + +typedef Future LibraryLoader(); +Map _deferredLibraries = { + 'de': () => new Future.value(null), + 'en': () => new Future.value(null), +}; + +MessageLookupByLibrary? _findExact(String localeName) { + switch (localeName) { + case 'de': + return messages_de.messages; + case 'en': + return messages_en.messages; + default: + return null; + } +} + +/// User programs should call this before using [localeName] for messages. +Future initializeMessages(String localeName) async { + var availableLocale = Intl.verifiedLocale( + localeName, (locale) => _deferredLibraries[locale] != null, + onFailure: (_) => null); + if (availableLocale == null) { + return new Future.value(false); + } + var lib = _deferredLibraries[availableLocale]; + await (lib == null ? new Future.value(false) : lib()); + initializeInternalMessageLookup(() => new CompositeMessageLookup()); + messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); + return new Future.value(true); +} + +bool _messagesExistFor(String locale) { + try { + return _findExact(locale) != null; + } catch (e) { + return false; + } +} + +MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { + var actualLocale = + Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); + if (actualLocale == null) return null; + return _findExact(actualLocale); +} diff --git a/lib/generated/intl/messages_de.dart b/lib/generated/intl/messages_de.dart new file mode 100644 index 0000000..f1a7753 --- /dev/null +++ b/lib/generated/intl/messages_de.dart @@ -0,0 +1,106 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a de locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'de'; + + static String m0(errorCode) => + "Ungültige Serverantwort: Fehlercode: ${errorCode}"; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "all_classes": MessageLookupByLibrary.simpleMessage("Alle Klassen"), + "back": MessageLookupByLibrary.simpleMessage("Zurück"), + "cancel": MessageLookupByLibrary.simpleMessage("Abbrechen"), + "create_class_hint": + MessageLookupByLibrary.simpleMessage("Klasse erstellen..."), + "edit_user": + MessageLookupByLibrary.simpleMessage("Benutzer bearbeiten"), + "email": MessageLookupByLibrary.simpleMessage("Email"), + "email_empty": MessageLookupByLibrary.simpleMessage("Email is empty"), + "failed_adding_user_invalid_id": MessageLookupByLibrary.simpleMessage( + "Benutzer konnte nicht erstellt werden: Datenbank ID ist ungültig."), + "failed_adding_user_on_track_invalid_id": + MessageLookupByLibrary.simpleMessage( + "Benutzer konnte nicht auf die Rennstrecke gestellt werden: ID ungültig."), + "failed_cancelling_lap_invalid_id": MessageLookupByLibrary.simpleMessage( + "Benutzer konnte nicht von der Rennstrecke entfernt werden: ID ungültig."), + "failed_finishing_lap_invalid_id": MessageLookupByLibrary.simpleMessage( + "Rundenzeit konnte nicht erfasst werden: Id oder Zeit ungültig."), + "failed_login": + MessageLookupByLibrary.simpleMessage("Login fehlgeschlagen."), + "failed_parsing_response": MessageLookupByLibrary.simpleMessage( + "Fehler beim Parsen der Serverantwort."), + "failed_removing_user_invalid_id": MessageLookupByLibrary.simpleMessage( + "Benutzer konnte nicht entfernt werden: Id ungültig."), + "failed_send_conflicting_data": MessageLookupByLibrary.simpleMessage( + "Konflikt in den gesendeten Daten."), + "failed_server_timeout": MessageLookupByLibrary.simpleMessage( + "Der Server kann nicht erreicht werden: Zeitüberschreitung."), + "failed_updating_user_invalid_data": + MessageLookupByLibrary.simpleMessage( + "Die Benutzerangaben sind unzureichend."), + "failed_updating_user_unexpected_response": + MessageLookupByLibrary.simpleMessage( + "Benutzer konnte nicht aktualisiert werden: Unerwartete Serverantwort."), + "first_name": MessageLookupByLibrary.simpleMessage("Vorname"), + "id": MessageLookupByLibrary.simpleMessage("ID"), + "invalid_server_response": m0, + "language": MessageLookupByLibrary.simpleMessage("Deutsch"), + "lap_time": MessageLookupByLibrary.simpleMessage("Zeit"), + "laps": MessageLookupByLibrary.simpleMessage("Runden"), + "last_name": MessageLookupByLibrary.simpleMessage("Nachname"), + "loading_user_error": MessageLookupByLibrary.simpleMessage( + "Fehler beim laden des Benutzers."), + "login_invalid": MessageLookupByLibrary.simpleMessage( + "Ungültiger Benutzername oder Passwort."), + "name": MessageLookupByLibrary.simpleMessage("Name"), + "no_permission": + MessageLookupByLibrary.simpleMessage("Fehlende Berechtigung."), + "ok_flat": MessageLookupByLibrary.simpleMessage("OK"), + "participants": MessageLookupByLibrary.simpleMessage("Teilnehmer"), + "password": MessageLookupByLibrary.simpleMessage("Password"), + "password_empty": + MessageLookupByLibrary.simpleMessage("Password is empty"), + "race_classes": MessageLookupByLibrary.simpleMessage("Rennklassen"), + "race_track": MessageLookupByLibrary.simpleMessage("Rennstrecke"), + "rank": MessageLookupByLibrary.simpleMessage("Rang"), + "ranking": MessageLookupByLibrary.simpleMessage("Rangliste"), + "requestet_entity_not_found": MessageLookupByLibrary.simpleMessage( + "Die Angeforderten Daten wurden nicht gefunden.."), + "retry": MessageLookupByLibrary.simpleMessage("Neu versuchen"), + "retry_login": MessageLookupByLibrary.simpleMessage( + "Bitte melden Sie sich erneut an."), + "save": MessageLookupByLibrary.simpleMessage("Speichern"), + "search_hint": MessageLookupByLibrary.simpleMessage("Suchen..."), + "session_expired": + MessageLookupByLibrary.simpleMessage("Sitzung abgelaufen"), + "session_expired_details": MessageLookupByLibrary.simpleMessage( + "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich neu an."), + "sign_in": MessageLookupByLibrary.simpleMessage("Login"), + "sign_in_title": MessageLookupByLibrary.simpleMessage("Racego login"), + "sync_errormessage": MessageLookupByLibrary.simpleMessage( + "Fehler: Synchronisation unterbrochen!"), + "unknown_error": + MessageLookupByLibrary.simpleMessage("Unbekannter Fehler"), + "unprocessable_entity": MessageLookupByLibrary.simpleMessage( + "Die Anfrage konnte nicht bearbeitet werden."), + "welcome": MessageLookupByLibrary.simpleMessage("Willkommen zurück") + }; +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart new file mode 100644 index 0000000..ad23665 --- /dev/null +++ b/lib/generated/intl/messages_en.dart @@ -0,0 +1,105 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a en locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'en'; + + static String m0(errorCode) => + "Invalid server response: Errorcode: ${errorCode}"; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "all_classes": MessageLookupByLibrary.simpleMessage("All classes"), + "back": MessageLookupByLibrary.simpleMessage("Back"), + "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), + "create_class_hint": + MessageLookupByLibrary.simpleMessage("Create Class..."), + "edit_user": MessageLookupByLibrary.simpleMessage("Edit User"), + "email": MessageLookupByLibrary.simpleMessage("Email"), + "email_empty": MessageLookupByLibrary.simpleMessage("Email is empty"), + "failed_adding_user_invalid_id": MessageLookupByLibrary.simpleMessage( + "User could not be created: Database ID is invalid."), + "failed_adding_user_on_track_invalid_id": + MessageLookupByLibrary.simpleMessage( + "User could not be placed on the race track: ID invalid."), + "failed_cancelling_lap_invalid_id": + MessageLookupByLibrary.simpleMessage( + "User could not be removed from the race track: ID invalid."), + "failed_finishing_lap_invalid_id": MessageLookupByLibrary.simpleMessage( + "Lap time could not be recorded: ID or time invalid."), + "failed_login": MessageLookupByLibrary.simpleMessage("Login failed."), + "failed_parsing_response": MessageLookupByLibrary.simpleMessage( + "Error parsing the server response."), + "failed_removing_user_invalid_id": MessageLookupByLibrary.simpleMessage( + "User could not be removed: ID invalid."), + "failed_send_conflicting_data": + MessageLookupByLibrary.simpleMessage("Conflict in the sent data."), + "failed_server_timeout": MessageLookupByLibrary.simpleMessage( + "The server cannot be reached: Timeout."), + "failed_updating_user_invalid_data": + MessageLookupByLibrary.simpleMessage( + "The user details are insufficient."), + "failed_updating_user_unexpected_response": + MessageLookupByLibrary.simpleMessage( + "User could not be updated: Unexpected server response."), + "first_name": MessageLookupByLibrary.simpleMessage("First Name"), + "id": MessageLookupByLibrary.simpleMessage("ID"), + "invalid_server_response": m0, + "language": MessageLookupByLibrary.simpleMessage("English"), + "lap_time": MessageLookupByLibrary.simpleMessage("Time"), + "laps": MessageLookupByLibrary.simpleMessage("Laps"), + "last_name": MessageLookupByLibrary.simpleMessage("Last Name"), + "loading_user_error": + MessageLookupByLibrary.simpleMessage("Error loading the user."), + "login_invalid": MessageLookupByLibrary.simpleMessage( + "Incorrect username or password."), + "name": MessageLookupByLibrary.simpleMessage("Name"), + "no_permission": + MessageLookupByLibrary.simpleMessage("No authorization."), + "ok_flat": MessageLookupByLibrary.simpleMessage("OK"), + "participants": MessageLookupByLibrary.simpleMessage("Participants"), + "password": MessageLookupByLibrary.simpleMessage("Password"), + "password_empty": + MessageLookupByLibrary.simpleMessage("Password is empty"), + "race_classes": MessageLookupByLibrary.simpleMessage("Race Classes"), + "race_track": MessageLookupByLibrary.simpleMessage("Race Track"), + "rank": MessageLookupByLibrary.simpleMessage("Rank"), + "ranking": MessageLookupByLibrary.simpleMessage("Ranking"), + "requestet_entity_not_found": MessageLookupByLibrary.simpleMessage( + "Requested entity was not found.."), + "retry": MessageLookupByLibrary.simpleMessage("Try again"), + "retry_login": + MessageLookupByLibrary.simpleMessage("Please login again"), + "save": MessageLookupByLibrary.simpleMessage("Save"), + "search_hint": MessageLookupByLibrary.simpleMessage("Search..."), + "session_expired": + MessageLookupByLibrary.simpleMessage("Session expired"), + "session_expired_details": MessageLookupByLibrary.simpleMessage( + "Your session has expired. Please log in again."), + "sign_in": MessageLookupByLibrary.simpleMessage("Login"), + "sign_in_title": MessageLookupByLibrary.simpleMessage("Racego login"), + "sync_errormessage": MessageLookupByLibrary.simpleMessage( + "Error: Synchronization interrupted!"), + "unknown_error": + MessageLookupByLibrary.simpleMessage("Unbekannter Fehler"), + "unprocessable_entity": MessageLookupByLibrary.simpleMessage( + "Couldn\'t process the request."), + "welcome": MessageLookupByLibrary.simpleMessage("Welcome back") + }; +} diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart new file mode 100644 index 0000000..6a6dbf6 --- /dev/null +++ b/lib/generated/l10n.dart @@ -0,0 +1,583 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'intl/messages_all.dart'; + +// ************************************************************************** +// Generator: Flutter Intl IDE plugin +// Made by Localizely +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars +// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each +// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes + +class S { + S(); + + static S? _current; + + static S get current { + assert(_current != null, + 'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.'); + return _current!; + } + + static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); + + static Future load(Locale locale) { + final name = (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); + final localeName = Intl.canonicalizedLocale(name); + return initializeMessages(localeName).then((_) { + Intl.defaultLocale = localeName; + final instance = S(); + S._current = instance; + + return instance; + }); + } + + static S of(BuildContext context) { + final instance = S.maybeOf(context); + assert(instance != null, + 'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?'); + return instance!; + } + + static S? maybeOf(BuildContext context) { + return Localizations.of(context, S); + } + + /// `English` + String get language { + return Intl.message( + 'English', + name: 'language', + desc: 'The current language', + args: [], + ); + } + + /// `Save` + String get save { + return Intl.message( + 'Save', + name: 'save', + desc: 'Display text for storing changes', + args: [], + ); + } + + /// `Cancel` + String get cancel { + return Intl.message( + 'Cancel', + name: 'cancel', + desc: 'Display text for cancelling changes', + args: [], + ); + } + + /// `Try again` + String get retry { + return Intl.message( + 'Try again', + name: 'retry', + desc: 'Try again text', + args: [], + ); + } + + /// `OK` + String get ok_flat { + return Intl.message( + 'OK', + name: 'ok_flat', + desc: 'ok label for flat buttons', + args: [], + ); + } + + /// `Back` + String get back { + return Intl.message( + 'Back', + name: 'back', + desc: 'Display text for going back', + args: [], + ); + } + + /// `Race Track` + String get race_track { + return Intl.message( + 'Race Track', + name: 'race_track', + desc: 'How a race track is called', + args: [], + ); + } + + /// `Participants` + String get participants { + return Intl.message( + 'Participants', + name: 'participants', + desc: 'How the participants are beeing called', + args: [], + ); + } + + /// `Race Classes` + String get race_classes { + return Intl.message( + 'Race Classes', + name: 'race_classes', + desc: 'How race classes are called', + args: [], + ); + } + + /// `Laps` + String get laps { + return Intl.message( + 'Laps', + name: 'laps', + desc: 'How laps are called', + args: [], + ); + } + + /// `ID` + String get id { + return Intl.message( + 'ID', + name: 'id', + desc: 'Name of an ID', + args: [], + ); + } + + /// `First Name` + String get first_name { + return Intl.message( + 'First Name', + name: 'first_name', + desc: 'How the first name is called', + args: [], + ); + } + + /// `Last Name` + String get last_name { + return Intl.message( + 'Last Name', + name: 'last_name', + desc: 'How the last name is called', + args: [], + ); + } + + /// `Racego login` + String get sign_in_title { + return Intl.message( + 'Racego login', + name: 'sign_in_title', + desc: 'Login title text', + args: [], + ); + } + + /// `Login` + String get sign_in { + return Intl.message( + 'Login', + name: 'sign_in', + desc: 'Login button text', + args: [], + ); + } + + /// `Please login again` + String get retry_login { + return Intl.message( + 'Please login again', + name: 'retry_login', + desc: 'Try login again text', + args: [], + ); + } + + /// `Email` + String get email { + return Intl.message( + 'Email', + name: 'email', + desc: 'Email name', + args: [], + ); + } + + /// `Email is empty` + String get email_empty { + return Intl.message( + 'Email is empty', + name: 'email_empty', + desc: 'Email is empty message', + args: [], + ); + } + + /// `Password` + String get password { + return Intl.message( + 'Password', + name: 'password', + desc: 'Password name', + args: [], + ); + } + + /// `Password is empty` + String get password_empty { + return Intl.message( + 'Password is empty', + name: 'password_empty', + desc: 'Password is empty message', + args: [], + ); + } + + /// `Incorrect username or password.` + String get login_invalid { + return Intl.message( + 'Incorrect username or password.', + name: 'login_invalid', + desc: 'Email or Password is incorrrect', + args: [], + ); + } + + /// `Welcome back` + String get welcome { + return Intl.message( + 'Welcome back', + name: 'welcome', + desc: 'Welcome message', + args: [], + ); + } + + /// `Session expired` + String get session_expired { + return Intl.message( + 'Session expired', + name: 'session_expired', + desc: 'Message if session expires', + args: [], + ); + } + + /// `Your session has expired. Please log in again.` + String get session_expired_details { + return Intl.message( + 'Your session has expired. Please log in again.', + name: 'session_expired_details', + desc: 'Detailed message if session expires', + args: [], + ); + } + + /// `Search...` + String get search_hint { + return Intl.message( + 'Search...', + name: 'search_hint', + desc: 'Hint text which is displayed for search bars', + args: [], + ); + } + + /// `Edit User` + String get edit_user { + return Intl.message( + 'Edit User', + name: 'edit_user', + desc: 'Edit user title', + args: [], + ); + } + + /// `Create Class...` + String get create_class_hint { + return Intl.message( + 'Create Class...', + name: 'create_class_hint', + desc: 'Hint text which is displayed for create class bars', + args: [], + ); + } + + /// `Ranking` + String get ranking { + return Intl.message( + 'Ranking', + name: 'ranking', + desc: 'Text translation for ranking', + args: [], + ); + } + + /// `All classes` + String get all_classes { + return Intl.message( + 'All classes', + name: 'all_classes', + desc: 'Text translation for \'all classes\'', + args: [], + ); + } + + /// `Time` + String get lap_time { + return Intl.message( + 'Time', + name: 'lap_time', + desc: '', + args: [], + ); + } + + /// `Name` + String get name { + return Intl.message( + 'Name', + name: 'name', + desc: 'Name of a \'name\' (independent of first or last name)\'', + args: [], + ); + } + + /// `Rank` + String get rank { + return Intl.message( + 'Rank', + name: 'rank', + desc: 'Name of a rank', + args: [], + ); + } + + /// `Error: Synchronization interrupted!` + String get sync_errormessage { + return Intl.message( + 'Error: Synchronization interrupted!', + name: 'sync_errormessage', + desc: 'Warning message if synchronization is interrupted', + args: [], + ); + } + + /// `Unbekannter Fehler` + String get unknown_error { + return Intl.message( + 'Unbekannter Fehler', + name: 'unknown_error', + desc: 'Error message for unknown errors', + args: [], + ); + } + + /// `Error loading the user.` + String get loading_user_error { + return Intl.message( + 'Error loading the user.', + name: 'loading_user_error', + desc: 'Error message if user couldn\'t be loaded', + args: [], + ); + } + + /// `User could not be removed from the race track: ID invalid.` + String get failed_cancelling_lap_invalid_id { + return Intl.message( + 'User could not be removed from the race track: ID invalid.', + name: 'failed_cancelling_lap_invalid_id', + desc: + 'Error message if user couldn\'t be removed from track, because of invalid ID', + args: [], + ); + } + + /// `Lap time could not be recorded: ID or time invalid.` + String get failed_finishing_lap_invalid_id { + return Intl.message( + 'Lap time could not be recorded: ID or time invalid.', + name: 'failed_finishing_lap_invalid_id', + desc: + 'Error message if lap could not be stored, because of invalid ID or lap-time', + args: [], + ); + } + + /// `User could not be removed: ID invalid.` + String get failed_removing_user_invalid_id { + return Intl.message( + 'User could not be removed: ID invalid.', + name: 'failed_removing_user_invalid_id', + desc: + 'Error message if user couldn\'t be removed -> because ID is invalid', + args: [], + ); + } + + /// `User could not be placed on the race track: ID invalid.` + String get failed_adding_user_on_track_invalid_id { + return Intl.message( + 'User could not be placed on the race track: ID invalid.', + name: 'failed_adding_user_on_track_invalid_id', + desc: 'Error message if user couldn\'t be placed on the track', + args: [], + ); + } + + /// `User could not be created: Database ID is invalid.` + String get failed_adding_user_invalid_id { + return Intl.message( + 'User could not be created: Database ID is invalid.', + name: 'failed_adding_user_invalid_id', + desc: 'Error message if user couldn\'t be created', + args: [], + ); + } + + /// `User could not be updated: Unexpected server response.` + String get failed_updating_user_unexpected_response { + return Intl.message( + 'User could not be updated: Unexpected server response.', + name: 'failed_updating_user_unexpected_response', + desc: + 'Error message if user couldn\'t be updated because of a unexpected server response', + args: [], + ); + } + + /// `Error parsing the server response.` + String get failed_parsing_response { + return Intl.message( + 'Error parsing the server response.', + name: 'failed_parsing_response', + desc: 'Error message if server response couldn\'t be parsed.', + args: [], + ); + } + + /// `Login failed.` + String get failed_login { + return Intl.message( + 'Login failed.', + name: 'failed_login', + desc: 'Error message if login wasn\'t successful.', + args: [], + ); + } + + /// `The user details are insufficient.` + String get failed_updating_user_invalid_data { + return Intl.message( + 'The user details are insufficient.', + name: 'failed_updating_user_invalid_data', + desc: 'Error message if user couldn\'t be updated', + args: [], + ); + } + + /// `No authorization.` + String get no_permission { + return Intl.message( + 'No authorization.', + name: 'no_permission', + desc: 'Error message if user has no permission', + args: [], + ); + } + + /// `Requested entity was not found..` + String get requestet_entity_not_found { + return Intl.message( + 'Requested entity was not found..', + name: 'requestet_entity_not_found', + desc: 'Error message if entity couldn\'t be found', + args: [], + ); + } + + /// `Conflict in the sent data.` + String get failed_send_conflicting_data { + return Intl.message( + 'Conflict in the sent data.', + name: 'failed_send_conflicting_data', + desc: '', + args: [], + ); + } + + /// `Couldn't process the request.` + String get unprocessable_entity { + return Intl.message( + 'Couldn\'t process the request.', + name: 'unprocessable_entity', + desc: 'Error message if send data couldn\'t be processed', + args: [], + ); + } + + /// `Invalid server response: Errorcode: {errorCode}` + String invalid_server_response(Object errorCode) { + return Intl.message( + 'Invalid server response: Errorcode: $errorCode', + name: 'invalid_server_response', + desc: 'Error message if the server response is invalid', + args: [errorCode], + ); + } + + /// `The server cannot be reached: Timeout.` + String get failed_server_timeout { + return Intl.message( + 'The server cannot be reached: Timeout.', + name: 'failed_server_timeout', + desc: 'Error message if the server cannot be reached', + args: [], + ); + } +} + +class AppLocalizationDelegate extends LocalizationsDelegate { + const AppLocalizationDelegate(); + + List get supportedLocales { + return const [ + Locale.fromSubtags(languageCode: 'en'), + Locale.fromSubtags(languageCode: 'de'), + ]; + } + + @override + bool isSupported(Locale locale) => _isSupported(locale); + @override + Future load(Locale locale) => S.load(locale); + @override + bool shouldReload(AppLocalizationDelegate old) => false; + + bool _isSupported(Locale locale) { + for (var supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale.languageCode) { + return true; + } + } + return false; + } +} diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb new file mode 100644 index 0000000..5a34171 --- /dev/null +++ b/lib/l10n/intl_de.arb @@ -0,0 +1,61 @@ +{ + "@@locale": "de", + "@________GENERAL________":{}, + "language": "Deutsch", + "save": "Speichern", + "cancel": "Abbrechen", + "retry": "Neu versuchen", + "ok_flat": "OK", + "back": "Zurück", + "@________RACEGO_SPECIFIC________":{}, + "race_track": "Rennstrecke", + "participants": "Teilnehmer", + "race_classes": "Rennklassen", + "laps": "Runden", + "id": "ID", + "first_name": "Vorname", + "last_name": "Nachname", + "@________LOGIN_SCREEN________":{}, + "sign_in_title": "Racego login", + "sign_in": "Login", + "retry_login": "Bitte melden Sie sich erneut an.", + "email": "Email", + "email_empty": "Email is empty", + "password": "Password", + "password_empty": "Password is empty", + "login_invalid": "Ungültiger Benutzername oder Passwort.", + "welcome": "Willkommen zurück", + "session_expired": "Sitzung abgelaufen", + "session_expired_details": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich neu an.", + "@________HOME_SCREEN________":{}, + "search_hint": "Suchen...", + "@________USER_SCREEN________":{}, + "edit_user": "Benutzer bearbeiten", + "create_class_hint": "Klasse erstellen...", + "@________RANKING_SCREEN________":{}, + "ranking": "Rangliste", + "all_classes": "Alle Klassen", + "lap_time": "Zeit", + "name": "Name", + "rank": "Rang", + "@________ERROR_MESSAGES________":{}, + "sync_errormessage": "Fehler: Synchronisation unterbrochen!", + "unknown_error": "Unbekannter Fehler", + "loading_user_error": "Fehler beim laden des Benutzers.", + "failed_cancelling_lap_invalid_id": "Benutzer konnte nicht von der Rennstrecke entfernt werden: ID ungültig.", + "failed_finishing_lap_invalid_id": "Rundenzeit konnte nicht erfasst werden: Id oder Zeit ungültig.", + "failed_removing_user_invalid_id": "Benutzer konnte nicht entfernt werden: Id ungültig.", + "failed_adding_user_on_track_invalid_id": "Benutzer konnte nicht auf die Rennstrecke gestellt werden: ID ungültig.", + "failed_adding_user_invalid_id": "Benutzer konnte nicht erstellt werden: Datenbank ID ist ungültig.", + "failed_updating_user_unexpected_response": "Benutzer konnte nicht aktualisiert werden: Unerwartete Serverantwort.", + "@________API_ERROR_MESSAGES________":{}, + "failed_parsing_response": "Fehler beim Parsen der Serverantwort.", + "failed_login": "Login fehlgeschlagen.", + "failed_updating_user_invalid_data": "Die Benutzerangaben sind unzureichend.", + "no_permission": "Fehlende Berechtigung.", + "requestet_entity_not_found": "Die Angeforderten Daten wurden nicht gefunden..", + "failed_send_conflicting_data": "Konflikt in den gesendeten Daten.", + "unprocessable_entity": "Die Anfrage konnte nicht bearbeitet werden.", + "invalid_server_response": "Ungültige Serverantwort: Fehlercode: {errorCode}", + "failed_server_timeout": "Der Server kann nicht erreicht werden: Zeitüberschreitung." +} \ No newline at end of file diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb new file mode 100644 index 0000000..faf99bf --- /dev/null +++ b/lib/l10n/intl_en.arb @@ -0,0 +1,212 @@ +{ + "@@locale": "en", + "@________GENERAL________":{}, + "language": "English", + "@language":{ + "description": "The current language" + }, + "save": "Save", + "@save":{ + "description": "Display text for storing changes" + }, + "cancel": "Cancel", + "@cancel":{ + "description": "Display text for cancelling changes" + }, + "retry": "Try again", + "@retry":{ + "description": "Try again text" + }, + "ok_flat": "OK", + "@ok_flat":{ + "description": "ok label for flat buttons" + }, + "back": "Back", + "@back":{ + "description": "Display text for going back" + }, + "@________RACEGO_SPECIFIC________":{}, + "race_track": "Race Track", + "@race_track":{ + "description": "How a race track is called" + }, + "participants": "Participants", + "@participants":{ + "description": "How the participants are beeing called" + }, + "race_classes": "Race Classes", + "@race_classes":{ + "description": "How race classes are called" + }, + "laps": "Laps", + "@laps":{ + "description": "How laps are called" + }, + "id": "ID", + "@id":{ + "description": "Name of an ID" + }, + "first_name": "First Name", + "@first_name":{ + "description": "How the first name is called" + }, + "last_name": "Last Name", + "@last_name":{ + "description": "How the last name is called" + }, + "@________LOGIN_SCREEN________":{}, + "sign_in_title": "Racego login", + "@sign_in_title":{ + "description": "Login title text" + }, + "sign_in": "Login", + "@sign_in":{ + "description": "Login button text" + }, + "retry_login": "Please login again", + "@retry_login":{ + "description": "Try login again text" + }, + "email": "Email", + "@email":{ + "description": "Email name" + }, + "email_empty": "Email is empty", + "@email_empty":{ + "description": "Email is empty message" + }, + "password": "Password", + "@password":{ + "description": "Password name" + }, + "password_empty": "Password is empty", + "@password_empty":{ + "description": "Password is empty message" + }, + "login_invalid": "Incorrect username or password.", + "@login_invalid":{ + "description": "Email or Password is incorrrect" + }, + "welcome": "Welcome back", + "@welcome":{ + "description": "Welcome message" + }, + "session_expired": "Session expired", + "@session_expired":{ + "description": "Message if session expires" + }, + "session_expired_details": "Your session has expired. Please log in again.", + "@session_expired_details":{ + "description": "Detailed message if session expires" + }, + "@________HOME_SCREEN________":{}, + "search_hint": "Search...", + "@search_hint":{ + "description": "Hint text which is displayed for search bars" + }, + "@________USER_SCREEN________":{}, + "edit_user": "Edit User", + "@edit_user":{ + "description": "Edit user title" + }, + "create_class_hint": "Create Class...", + "@create_class_hint":{ + "description": "Hint text which is displayed for create class bars" + }, + "@________RANKING_SCREEN________":{}, + "ranking": "Ranking", + "@ranking":{ + "description": "Text translation for ranking" + }, + "all_classes": "All classes", + "@all_classes":{ + "description": "Text translation for 'all classes'" + }, + "lap_time": "Time", + "@time":{ + "description": "Name of a single lap time'" + }, + "name": "Name", + "@name":{ + "description": "Name of a 'name' (independent of first or last name)'" + }, + "rank": "Rank", + "@rank":{ + "description": "Name of a rank" + }, + "@________ERROR_MESSAGES________":{}, + "sync_errormessage": "Error: Synchronization interrupted!", + "@sync_errormessage":{ + "description": "Warning message if synchronization is interrupted" + }, + "unknown_error": "Unbekannter Fehler", + "@unknown_error":{ + "description": "Error message for unknown errors" + }, + "loading_user_error": "Error loading the user.", + "@loading_user_error":{ + "description": "Error message if user couldn't be loaded" + }, + "failed_cancelling_lap_invalid_id": "User could not be removed from the race track: ID invalid.", + "@failed_cancelling_lap_invalid_id":{ + "description": "Error message if user couldn't be removed from track, because of invalid ID" + }, + "failed_finishing_lap_invalid_id": "Lap time could not be recorded: ID or time invalid.", + "@failed_finishing_lap_invalid_id":{ + "description": "Error message if lap could not be stored, because of invalid ID or lap-time" + }, + "failed_removing_user_invalid_id": "User could not be removed: ID invalid.", + "@failed_removing_user_invalid_id":{ + "description": "Error message if user couldn't be removed -> because ID is invalid" + }, + "failed_adding_user_on_track_invalid_id": "User could not be placed on the race track: ID invalid.", + "@failed_adding_user_on_track_invalid_id":{ + "description": "Error message if user couldn't be placed on the track" + }, + "failed_adding_user_invalid_id": "User could not be created: Database ID is invalid.", + "@failed_adding_user_invalid_id":{ + "description": "Error message if user couldn't be created" + }, + "failed_updating_user_unexpected_response": "User could not be updated: Unexpected server response.", + "@failed_updating_user_unexpected_response":{ + "description": "Error message if user couldn't be updated because of a unexpected server response" + }, + "@________API_ERROR_MESSAGES________":{}, + "failed_parsing_response": "Error parsing the server response.", + "@failed_parsing_response":{ + "description": "Error message if server response couldn't be parsed." + }, + "failed_login": "Login failed.", + "@failed_login":{ + "description": "Error message if login wasn't successful." + }, + "failed_updating_user_invalid_data": "The user details are insufficient.", + "@failed_updating_user_invalid_data":{ + "description": "Error message if user couldn't be updated" + }, + "no_permission": "No authorization.", + "@no_permission":{ + "description": "Error message if user has no permission" + }, + "requestet_entity_not_found": "Requested entity was not found..", + "@requestet_entity_not_found":{ + "description": "Error message if entity couldn't be found" + }, + "failed_send_conflicting_data": "Conflict in the sent data.", + "@failed_conflicting_data":{ + "description": "Error message if send data produces conflict" + }, + "unprocessable_entity": "Couldn't process the request.", + "@unprocessable_entity":{ + "description": "Error message if send data couldn't be processed" + }, + "invalid_server_response": "Invalid server response: Errorcode: {errorCode}", + "@invalid_server_response":{ + "description": "Error message if the server response is invalid" + }, + "failed_server_timeout": "The server cannot be reached: Timeout.", + "@failed_server_timeout":{ + "description": "Error message if the server cannot be reached" + } +} + diff --git a/lib/main.dart b/lib/main.dart index 554ee44..61be60d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:racego/business_logic/login/login_bloc.dart'; import 'package:racego/ui/screens/homescreen.dart'; +import 'package:racego/ui/screens/rankingscreen.dart'; import 'package:racego/ui/screens/userscreen.dart'; import 'ui/screens/loginscreen.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:racego/data/provider/provider.dart'; +import 'package:window_size/window_size.dart'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:racego/generated/l10n.dart'; +import 'package:racego/ui/themes/darktheme.dart'; +import 'package:racego/ui/themes/lighttheme.dart'; void main() { + WidgetsFlutterBinding.ensureInitialized(); + if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { + setWindowTitle('Racego'); + setWindowMinSize(const Size(950, 650)); + } runApp( setupProvider(child: const Racego()), ); @@ -18,6 +31,13 @@ class Racego extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, onGenerateRoute: (settings) { if (settings.name == '/') { if (context.read().state is LoggedIn) { @@ -35,12 +55,18 @@ class Racego extends StatelessWidget { } else { return MaterialPageRoute(builder: (_) => const UserScreen()); } + } else if (settings.name == '/ranking') { + return MaterialPageRoute(builder: (_) => const RankingScreen()); } + return null; // Let `onUnknownRoute` handle this behavior. }, - theme: ThemeData(), - darkTheme: ThemeData.dark(), - themeMode: ThemeMode.dark, + builder: (context, child) { + return child!; + }, + theme: lightTheme, + darkTheme: darkTheme, + themeMode: ThemeMode.system, title: 'Racego', ); } diff --git a/lib/ui/screens/homescreen.dart b/lib/ui/screens/homescreen.dart index 6cd0fcd..344c236 100644 --- a/lib/ui/screens/homescreen.dart +++ b/lib/ui/screens/homescreen.dart @@ -1,3 +1,4 @@ +import 'package:another_flushbar/flushbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:racego/business_logic/login/login_bloc.dart'; @@ -12,6 +13,7 @@ import 'package:racego/ui/widgets/user_list.dart'; import 'package:racego/ui/widgets/timeinput.dart'; import 'package:racego/business_logic/listtoolbar/listtoolbar_cubit.dart'; import 'package:racego/ui/widgets/coloredbutton.dart'; +import 'package:racego/generated/l10n.dart'; import '../widgets/loggedoutdialog.dart'; @@ -92,16 +94,14 @@ class _HomeScreenState extends State { bloc: _userlistCubit, builder: (context, state) { if (state is listcubit.Error && state.syncError) { - return _processHardError( - 'Fehler: Synchronisation unterbrochen!'); + return _processHardError(S.current.sync_errormessage); } else { return BlocBuilder( bloc: _tracklistCubit, builder: (context, state) { if (state is trackcubit.Error && state.syncError) { - return _processHardError( - 'Fehler: Synchronisation unterbrochen!'); + return _processHardError(S.current.sync_errormessage); } else { return const SizedBox(); } @@ -122,7 +122,6 @@ class _HomeScreenState extends State { padding: const EdgeInsets.all(2), child: Image.asset('assets/racego_r.png', fit: BoxFit.cover), ), - backgroundColor: const Color.fromARGB(255, 175, 0, 6), title: const Text( 'Racego', ), @@ -136,7 +135,12 @@ class _HomeScreenState extends State { _forcedLogout = true; context.read().add(Logout()); }, - ) + ), + const SizedBox(width: 5), + IconButton( + icon: const Icon(Icons.emoji_events_rounded), + onPressed: () => Navigator.of(context).pushNamed('/ranking'), + ), ], ); } @@ -145,9 +149,9 @@ class _HomeScreenState extends State { return Column( children: [ const SizedBox(height: 30), - const Text( - 'Willkommen zurück', - style: TextStyle( + Text( + S.current.welcome, + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 50, ), @@ -180,7 +184,7 @@ class _HomeScreenState extends State { return UserList( state.list, searchChanged: (text) => _userlistCubit.setFilter(text), - title: 'Teilnehmer', + title: S.current.participants, onSelectionChanged: (index, userID, isSelected) { isSelected ? _userToolsCubit.selectionChanged(userID) @@ -201,7 +205,7 @@ class _HomeScreenState extends State { return UserList( newList, searchChanged: (text) => _userlistCubit.setFilter(text), - title: 'Teilnehmer', + title: S.current.participants, onSelectionChanged: (index, userID, isSelected) { isSelected ? _userToolsCubit.selectionChanged(userID) @@ -216,6 +220,7 @@ class _HomeScreenState extends State { }, ), ), + const SizedBox(height: 5), BlocBuilder( bloc: _userToolsCubit, builder: (context, state) { @@ -276,7 +281,7 @@ class _HomeScreenState extends State { return UserList( state.list, searchChanged: (text) => _tracklistCubit.setFilter(text), - title: 'Rennstrecke', + title: S.current.race_track, onSelectionChanged: (index, userID, isSelected) { isSelected ? _trackToolsCubit.selectionChanged(userID) @@ -295,7 +300,7 @@ class _HomeScreenState extends State { return UserList( newList, searchChanged: (text) => _tracklistCubit.setFilter(text), - title: 'Rennstrecke', + title: S.current.race_track, onSelectionChanged: (index, userID, isSelected) { isSelected ? _trackToolsCubit.selectionChanged(userID) @@ -308,6 +313,7 @@ class _HomeScreenState extends State { }, ), ), + const SizedBox(height: 5), BlocBuilder( bloc: _trackToolsCubit, builder: (context, state) { @@ -399,16 +405,27 @@ class _HomeScreenState extends State { context.read().add(Logout()); return; } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - behavior: SnackBarBehavior.floating, - duration: const Duration(seconds: 5), - content: Text(exception.errorMessage), - action: SnackBarAction( - label: 'OK', - onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(), + + Flushbar( + animationDuration: const Duration(milliseconds: 500), + margin: const EdgeInsets.all(8), + borderRadius: BorderRadius.circular(8), + message: exception.errorMessage, + duration: const Duration(seconds: 5), + flushbarPosition: FlushbarPosition.TOP, + isDismissible: true, + icon: const Icon( + Icons.warning_amber_rounded, + size: 28.0, + color: Colors.red, + ), + mainButton: TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + S.of(context).ok_flat, + style: const TextStyle(color: Colors.blue), ), ), - ); + ).show(context); } } diff --git a/lib/ui/screens/loginscreen.dart b/lib/ui/screens/loginscreen.dart index 0b2ad6e..fa92ac0 100644 --- a/lib/ui/screens/loginscreen.dart +++ b/lib/ui/screens/loginscreen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:racego/business_logic/login/login_bloc.dart'; import 'package:racego/ui/widgets/coloredbutton.dart'; +import 'package:racego/generated/l10n.dart'; class LoginPage extends StatefulWidget { const LoginPage({Key? key}) : super(key: key); @@ -29,9 +30,9 @@ class _LoginPageState extends State { child: SizedBox( child: Column( children: [ - const Text( - 'Racego Login', - style: TextStyle(fontSize: 20), + Text( + S.current.sign_in_title, + style: const TextStyle(fontSize: 20), textAlign: TextAlign.center, ), const SizedBox( @@ -54,7 +55,7 @@ class _LoginPageState extends State { width: 300, ), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.1), + color: Theme.of(context).colorScheme.onBackground, borderRadius: const BorderRadius.all(Radius.circular(4)), ), padding: const EdgeInsets.all(40), @@ -92,10 +93,10 @@ class _LoginPageState extends State { } }, decoration: InputDecoration( - errorText: _showEmailEmptyMessage ? 'Email ist leer' : null, + errorText: _showEmailEmptyMessage ? S.current.email_empty : null, border: const OutlineInputBorder(), - hintText: 'Email', - labelText: 'Email', + hintText: S.current.email, + labelText: S.current.email, ), ); } @@ -127,10 +128,10 @@ class _LoginPageState extends State { } }, decoration: InputDecoration( - errorText: _showPasswordEmptyMessage ? 'Passwort ist leer' : null, + errorText: _showPasswordEmptyMessage ? S.current.password_empty : null, border: const OutlineInputBorder(), - hintText: 'Passwort', - labelText: 'Passwort', + hintText: S.current.password, + labelText: S.current.password, ), ); } @@ -145,9 +146,9 @@ class _LoginPageState extends State { children: [ SizedBox( child: ColoredButton( - const Text( - 'Anmelden', - style: TextStyle( + Text( + S.current.sign_in, + style: const TextStyle( color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold, @@ -178,9 +179,9 @@ class _LoginPageState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ ColoredButton( - const Text( - 'Anmelden', - style: TextStyle( + Text( + S.current.sign_in, + style: const TextStyle( color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold, @@ -197,7 +198,7 @@ class _LoginPageState extends State { ], ); } else { - return const Text('Unbekannter Fehler'); + return Text(S.current.unknown_error); } }, ); diff --git a/lib/ui/screens/rankingscreen.dart b/lib/ui/screens/rankingscreen.dart new file mode 100644 index 0000000..98838d4 --- /dev/null +++ b/lib/ui/screens/rankingscreen.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:racego/business_logic/login/login_bloc.dart'; +import 'package:racego/ui/widgets/coloredbutton.dart'; +import 'package:racego/ui/widgets/loggedoutdialog.dart'; +import 'package:racego/generated/l10n.dart'; +import 'package:racego/ui/widgets/ranking.dart'; + +class RankingScreen extends StatefulWidget { + const RankingScreen({Key? key, int? userId}) : super(key: key); + + @override + _RankingScreenState createState() => _RankingScreenState(); +} + +class _RankingScreenState extends State { + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) async { + if (state is LoggedOut || state is LoginError) { + await loggedOutDialog(context); + WidgetsBinding.instance?.addPostFrameCallback((_) { + Navigator.pushReplacementNamed(context, '/'); + }); + } + }, + child: Scaffold( + appBar: _appBar(), + body: Column( + children: [ + const SizedBox(height: 20), + Expanded( + child: Center( + child: Container( + margin: + const EdgeInsets.symmetric(vertical: 10, horizontal: 50), + width: 800, + child: Ranking( + onAuthException: () => + context.read().add(Logout()), + ), + ), + ), + ), + _bottomBar(), + const SizedBox(height: 20), + ], + ), + ), + ); + } + + AppBar _appBar() { + return AppBar( + leading: Padding( + padding: const EdgeInsets.all(2), + child: Image.asset('assets/racego_r.png', fit: BoxFit.cover), + ), + automaticallyImplyLeading: false, + title: Text( + S.current.ranking, + ), + centerTitle: true, + ); + } + + Widget _bottomBar() { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ColoredButton( + Text( + S.current.back, + style: const TextStyle( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.bold, + ), + ), + onPressed: () => Navigator.of(context).pop(), + color: Colors.red, + ), + const SizedBox(width: 20), + ], + ); + } +} diff --git a/lib/ui/screens/userscreen.dart b/lib/ui/screens/userscreen.dart index 732bb33..c6a6149 100644 --- a/lib/ui/screens/userscreen.dart +++ b/lib/ui/screens/userscreen.dart @@ -1,3 +1,4 @@ +import 'package:another_flushbar/flushbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:racego/business_logic/login/login_bloc.dart'; @@ -9,6 +10,7 @@ import 'package:racego/data/models/userdetails.dart'; import 'package:racego/ui/widgets/coloredbutton.dart'; import 'package:racego/ui/widgets/lapseditor.dart'; import 'package:racego/ui/widgets/loggedoutdialog.dart'; +import 'package:racego/generated/l10n.dart'; import '../widgets/selectablelist.dart'; @@ -75,20 +77,29 @@ class _UserScreenState extends State { context.read().add(Logout()); return; } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - behavior: SnackBarBehavior.floating, - duration: const Duration(seconds: 5), - content: Text(exception != null - ? exception.errorMessage - : 'Unbekannter Fehler'), - action: SnackBarAction( - label: 'OK', - onPressed: () => - ScaffoldMessenger.of(context).hideCurrentSnackBar(), + Flushbar( + animationDuration: const Duration(milliseconds: 500), + margin: const EdgeInsets.all(8), + borderRadius: BorderRadius.circular(8), + message: exception != null + ? exception.errorMessage + : S.current.unknown_error, + duration: const Duration(seconds: 5), + flushbarPosition: FlushbarPosition.TOP, + isDismissible: true, + icon: const Icon( + Icons.warning_amber_rounded, + size: 28.0, + color: Colors.red, + ), + mainButton: TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + S.of(context).ok_flat, + style: const TextStyle(color: Colors.blue), ), ), - ); + ).show(context); } }, ), @@ -100,9 +111,8 @@ class _UserScreenState extends State { child: Image.asset('assets/racego_r.png', fit: BoxFit.cover), ), automaticallyImplyLeading: false, - backgroundColor: const Color.fromARGB(255, 175, 0, 6), - title: const Text( - 'Benutzer bearbeiten', + title: Text( + S.current.edit_user, ), centerTitle: true, ), @@ -141,9 +151,9 @@ class _UserScreenState extends State { state is UserScreenEditSaving; return ColoredButton( - const Text( - 'Speichern', - style: TextStyle( + Text( + S.current.save, + style: const TextStyle( color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold, @@ -168,9 +178,9 @@ class _UserScreenState extends State { ), const SizedBox(width: 20), ColoredButton( - const Text( - 'Zurück', - style: TextStyle( + Text( + S.current.back, + style: const TextStyle( color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold, @@ -213,12 +223,12 @@ class _UserScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('Fehler beim Laden des Benutzers. Neu versuchen?'), + Text(S.current.loading_user_error), const SizedBox(height: 20), ColoredButton( - const Text( - 'Neu versuchen', - style: TextStyle( + Text( + S.current.retry, + style: const TextStyle( color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold, @@ -249,20 +259,21 @@ class _UserScreenState extends State { ), Container( padding: const EdgeInsets.fromLTRB(20, 10, 20, 30), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(4)), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(4)), ), width: 300, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextField( - decoration: const InputDecoration(hintText: 'Vorname'), + decoration: + InputDecoration(hintText: S.current.first_name), controller: _firstName, ), TextField( - decoration: const InputDecoration(hintText: 'Nachname'), + decoration: + InputDecoration(hintText: S.current.last_name), controller: _lastName, ), ], diff --git a/lib/ui/themes/darktheme.dart b/lib/ui/themes/darktheme.dart new file mode 100644 index 0000000..7377993 --- /dev/null +++ b/lib/ui/themes/darktheme.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +final ThemeData darkTheme = ThemeData( + iconTheme: const IconThemeData(color: Colors.white), + // background color + scaffoldBackgroundColor: Colors.grey.shade900, + // background for all material widgets + canvasColor: Colors.grey[850]!, + colorScheme: ColorScheme( + brightness: Brightness.dark, + background: Colors.grey.shade900, + onBackground: Colors.grey[850]!, + primary: Colors.grey.shade700, + onPrimary: Colors.white, + secondary: Colors.grey.shade700, + onSecondary: Colors.white, + error: Colors.red, + onError: Colors.white, + surface: Colors.grey.shade800, + onSurface: Colors.white, + ), + // default hover color (for appbar icons) + hoverColor: Colors.black.withOpacity(0.1), + // row selection color + selectedRowColor: Colors.grey.shade700, + // text-color for hint-texts + hintColor: Colors.white, + // default text sizes and styles + textTheme: const TextTheme( + // body text style + bodyText2: TextStyle(color: Colors.white), + // title text style + headline6: TextStyle( + fontSize: 25, fontWeight: FontWeight.bold, color: Colors.white), + // textfield text-color + subtitle1: TextStyle(color: Colors.white), + ), + // text selection style + textSelectionTheme: TextSelectionThemeData( + cursorColor: Colors.white, + selectionColor: Colors.blue.shade800, + ), + appBarTheme: const AppBarTheme( + color: Color.fromARGB(255, 175, 0, 6), + ), + inputDecorationTheme: InputDecorationTheme( + fillColor: Colors.grey.shade800, + filled: true, + hoverColor: Colors.grey.shade700, + labelStyle: const TextStyle(color: Colors.white), + ), +); diff --git a/lib/ui/themes/lighttheme.dart b/lib/ui/themes/lighttheme.dart new file mode 100644 index 0000000..b655acd --- /dev/null +++ b/lib/ui/themes/lighttheme.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +final ThemeData lightTheme = ThemeData( + iconTheme: IconThemeData(color: Colors.grey.shade900), + // background color + scaffoldBackgroundColor: Colors.white, + // background for all material widgets + canvasColor: Colors.white, + colorScheme: ColorScheme( + brightness: Brightness.dark, + background: Colors.white, + onBackground: Colors.white, + primary: Colors.grey.shade700, + onPrimary: Colors.white, + secondary: Colors.grey.shade700, + onSecondary: Colors.white, + error: Colors.red, + onError: Colors.white, + surface: Colors.grey.shade800, + onSurface: Colors.white, + ), + // default hover color (for appbar icons) + hoverColor: Colors.black.withOpacity(0.1), + // row selection color + selectedRowColor: Colors.grey.shade300, + // text-color for hint-texts + hintColor: Colors.black, + // default text sizes and styles + textTheme: const TextTheme( + // body text style + bodyText2: TextStyle(color: Colors.black), + // title text style + headline6: TextStyle( + fontSize: 25, fontWeight: FontWeight.bold, color: Colors.black), + // textfield text-color + subtitle1: TextStyle(color: Colors.black), + ), + // text selection style + textSelectionTheme: TextSelectionThemeData( + cursorColor: Colors.black, + selectionColor: Colors.blue.shade800, + ), + appBarTheme: const AppBarTheme( + color: Color.fromARGB(255, 175, 0, 6), + ), + inputDecorationTheme: InputDecorationTheme( + fillColor: Colors.grey.shade400, + filled: true, + hoverColor: Colors.grey.shade500, + labelStyle: const TextStyle(color: Colors.white), + ), +); diff --git a/lib/ui/widgets/coloredbutton.dart b/lib/ui/widgets/coloredbutton.dart index 0952559..d15e1a4 100644 --- a/lib/ui/widgets/coloredbutton.dart +++ b/lib/ui/widgets/coloredbutton.dart @@ -24,7 +24,7 @@ class ColoredButton extends StatelessWidget { return Container( foregroundDecoration: isLoading ? BoxDecoration( - color: Colors.grey.withOpacity(0.2), + color: color ?? Colors.grey, backgroundBlendMode: BlendMode.saturation, ) : null, diff --git a/lib/ui/widgets/dropdownmenu.dart b/lib/ui/widgets/dropdownmenu.dart new file mode 100644 index 0000000..38ea1a8 --- /dev/null +++ b/lib/ui/widgets/dropdownmenu.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +class DropDownMenu extends StatefulWidget { + const DropDownMenu( + {Key? key, + List? items, + this.selectionChanged, + this.initialSelection}) + : items = items ?? const [], + super(key: key); + + final List items; + final Function(String?)? selectionChanged; + final String? initialSelection; + + @override + State createState() => _DropDownMenuState(); +} + +class _DropDownMenuState extends State { + String? _currentItem; + + @override + void initState() { + // try to select initial item and set it to _currentSelection + if (widget.initialSelection != null && + widget.items.contains(widget.initialSelection)) { + _currentItem = widget.initialSelection; + widget.selectionChanged?.call(widget.initialSelection); + } + super.initState(); + } + + @override + void didChangeDependencies() { + // if the list changes, this function will re-validate the current selection + if (!widget.items.contains(_currentItem)) { + // try to set initial selection + if (widget.initialSelection != null && + widget.items.contains(widget.initialSelection)) { + _currentItem = widget.initialSelection; + widget.selectionChanged?.call(widget.initialSelection); + } else { + // try to select first item + if (widget.items.isNotEmpty) { + _currentItem = widget.items.first; + widget.selectionChanged?.call(_currentItem = widget.items.first); + } + // else reset selection (no selection) + else { + _currentItem = null; + widget.selectionChanged?.call(null); + } + } + } + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return DropdownButtonFormField( + focusColor: Colors.transparent, + value: _currentItem, + icon: const Icon( + Icons.arrow_downward, + size: 20, + ), + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.all(10), + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(4)), + ), + ), + onChanged: (String? newValue) { + _currentItem = newValue; + setState(() { + widget.selectionChanged?.call(newValue); + _currentItem = newValue!; + }); + }, + items: widget.items.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ); + } +} diff --git a/lib/ui/widgets/lapseditor.dart b/lib/ui/widgets/lapseditor.dart index 39d553e..4c8283a 100644 --- a/lib/ui/widgets/lapseditor.dart +++ b/lib/ui/widgets/lapseditor.dart @@ -4,6 +4,7 @@ import 'package:racego/business_logic/lapeditor_cubit/lapeditor_cubit.dart'; import 'package:racego/data/models/time.dart'; import 'package:racego/ui/widgets/coloredbutton.dart'; import 'package:racego/ui/widgets/timeinput.dart'; +import 'package:racego/generated/l10n.dart'; class LapsEditor extends StatefulWidget { const LapsEditor(this.laps, {Key? key}) : super(key: key); @@ -26,7 +27,9 @@ class _LapsEditorState extends State { return Column( children: [ _title(), + const SizedBox(height: 5), Expanded(child: _list()), + const SizedBox(height: 5), _toolButtons(), ], ); @@ -34,15 +37,25 @@ class _LapsEditorState extends State { Widget _title() { return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onBackground, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + spreadRadius: 2, + blurRadius: 5, + // offset: const Offset(0, 3), // changes position of shadow + ), + ], + ), padding: const EdgeInsets.all(10), - color: Colors.grey.shade800.withOpacity(0.7), width: double.infinity, child: BlocBuilder( bloc: _cubit, buildWhen: (previous, current) => current is LapsChanged, builder: (index, state) { return Text( - 'Runden: ${_cubit.laps.length}', + S.current.laps + ': ${_cubit.laps.length}', textAlign: TextAlign.center, style: const TextStyle( fontSize: 20, @@ -64,17 +77,30 @@ class _LapsEditorState extends State { if (state is LapsChanged) { final List