diff --git a/fastlane/metadata/android/el-GR/images/phoneScreenshots/01 - dashboard.png b/fastlane/metadata/android/el-GR/images/phoneScreenshots/01 - dashboard.png new file mode 100644 index 00000000..a56b3002 Binary files /dev/null and b/fastlane/metadata/android/el-GR/images/phoneScreenshots/01 - dashboard.png differ diff --git a/fastlane/metadata/android/el-GR/images/phoneScreenshots/02 - workout detail.png b/fastlane/metadata/android/el-GR/images/phoneScreenshots/02 - workout detail.png new file mode 100644 index 00000000..c5b80a25 Binary files /dev/null and b/fastlane/metadata/android/el-GR/images/phoneScreenshots/02 - workout detail.png differ diff --git a/fastlane/metadata/android/el-GR/images/phoneScreenshots/03 - gym mode.png b/fastlane/metadata/android/el-GR/images/phoneScreenshots/03 - gym mode.png new file mode 100644 index 00000000..0af70e46 Binary files /dev/null and b/fastlane/metadata/android/el-GR/images/phoneScreenshots/03 - gym mode.png differ diff --git a/fastlane/metadata/android/el-GR/images/phoneScreenshots/04 - measurements.png b/fastlane/metadata/android/el-GR/images/phoneScreenshots/04 - measurements.png new file mode 100644 index 00000000..f171872b Binary files /dev/null and b/fastlane/metadata/android/el-GR/images/phoneScreenshots/04 - measurements.png differ diff --git a/fastlane/metadata/android/el-GR/images/phoneScreenshots/05 - nutritional plan.png b/fastlane/metadata/android/el-GR/images/phoneScreenshots/05 - nutritional plan.png new file mode 100644 index 00000000..ae36c68e Binary files /dev/null and b/fastlane/metadata/android/el-GR/images/phoneScreenshots/05 - nutritional plan.png differ diff --git a/fastlane/metadata/android/el-GR/images/phoneScreenshots/06 - weight.png b/fastlane/metadata/android/el-GR/images/phoneScreenshots/06 - weight.png new file mode 100644 index 00000000..e0d4d2bc Binary files /dev/null and b/fastlane/metadata/android/el-GR/images/phoneScreenshots/06 - weight.png differ diff --git a/fastlane/metadata/android/el-GR/images/tenInchScreenshots/01 - dashboard.png b/fastlane/metadata/android/el-GR/images/tenInchScreenshots/01 - dashboard.png new file mode 100644 index 00000000..bf59e6e4 Binary files /dev/null and b/fastlane/metadata/android/el-GR/images/tenInchScreenshots/01 - dashboard.png differ diff --git a/fastlane/metadata/android/el-GR/images/tenInchScreenshots/02 - workout detail.png b/fastlane/metadata/android/el-GR/images/tenInchScreenshots/02 - workout detail.png new file mode 100644 index 00000000..e90e7775 Binary files /dev/null and b/fastlane/metadata/android/el-GR/images/tenInchScreenshots/02 - workout detail.png differ diff --git a/fastlane/metadata/android/el-GR/images/tenInchScreenshots/03 - gym mode.png b/fastlane/metadata/android/el-GR/images/tenInchScreenshots/03 - gym mode.png new file mode 100644 index 00000000..825c85b0 Binary files /dev/null and b/fastlane/metadata/android/el-GR/images/tenInchScreenshots/03 - gym mode.png differ diff --git a/fastlane/metadata/android/el-GR/images/tenInchScreenshots/04 - measurements.png b/fastlane/metadata/android/el-GR/images/tenInchScreenshots/04 - measurements.png new file mode 100644 index 00000000..4bdab99c Binary files /dev/null and b/fastlane/metadata/android/el-GR/images/tenInchScreenshots/04 - measurements.png differ diff --git a/fastlane/metadata/android/el-GR/images/tenInchScreenshots/05 - nutritional plan.png b/fastlane/metadata/android/el-GR/images/tenInchScreenshots/05 - nutritional plan.png new file mode 100644 index 00000000..8eb4ab45 Binary files /dev/null and b/fastlane/metadata/android/el-GR/images/tenInchScreenshots/05 - nutritional plan.png differ diff --git a/fastlane/metadata/android/el-GR/images/tenInchScreenshots/06 - weight.png b/fastlane/metadata/android/el-GR/images/tenInchScreenshots/06 - weight.png new file mode 100644 index 00000000..c75b6946 Binary files /dev/null and b/fastlane/metadata/android/el-GR/images/tenInchScreenshots/06 - weight.png differ diff --git a/flatpak/scripts/flatpak_shared.dart b/flatpak/scripts/flatpak_shared.dart index f9432f5c..4676cdf8 100644 --- a/flatpak/scripts/flatpak_shared.dart +++ b/flatpak/scripts/flatpak_shared.dart @@ -103,7 +103,7 @@ class GithubReleases { if (releases.isNotEmpty || canBeEmpty) { _releases = releases; } else { - throw Exception("Github must contain at least 1 release."); + throw Exception('Github must contain at least 1 release.'); } } @@ -223,7 +223,7 @@ class FlatpakMeta { final releases = List.empty(growable: true); if (addedTodaysVersion != null) { releases.add(Release( - version: addedTodaysVersion, date: DateTime.now().toIso8601String().split("T").first)); + version: addedTodaysVersion, date: DateTime.now().toIso8601String().split('T').first)); } if (fetchReleasesFromGithub) { if (_githubReleases == null) { diff --git a/lib/exceptions/http_exception.dart b/lib/exceptions/http_exception.dart index b442e34a..ca38f0a2 100644 --- a/lib/exceptions/http_exception.dart +++ b/lib/exceptions/http_exception.dart @@ -29,7 +29,8 @@ class WgerHttpException implements Exception { errors = {'unknown_error': 'An unknown error occurred, no further information available'}; } else { try { - errors = {'unknown_error': json.decode(responseBody)}; + final response = json.decode(responseBody); + errors = (response is Map ? response : {'unknown_error': response}).cast(); } catch (e) { errors = {'unknown_error': responseBody}; } diff --git a/lib/helpers/ui.dart b/lib/helpers/ui.dart index e6139b8c..d96b4b5b 100644 --- a/lib/helpers/ui.dart +++ b/lib/helpers/ui.dart @@ -28,9 +28,9 @@ import 'package:wger/models/workouts/log.dart'; import 'package:wger/providers/workout_plans.dart'; void showErrorDialog(dynamic exception, BuildContext context) { - log('showErrorDialog: '); - log(exception.toString()); - log('====================='); + // log('showErrorDialog: '); + // log(exception.toString()); + // log('====================='); showDialog( context: context, @@ -55,6 +55,7 @@ void showHttpExceptionErrorDialog(WgerHttpException exception, BuildContext cont log('-------------------'); final List errorList = []; + for (final key in exception.errors!.keys) { // Error headers // Ensure that the error heading first letter is capitalized. diff --git a/lib/l10n/app_hr.arb b/lib/l10n/app_hr.arb index 8dd56075..2b98be30 100644 --- a/lib/l10n/app_hr.arb +++ b/lib/l10n/app_hr.arb @@ -401,7 +401,7 @@ "@aboutBugsTitle": { "description": "Title for bugs section in the about dialog" }, - "aboutContactUsText": "Ako želi razgovarati s nama, skoči na Discord poslužitelj i kontaktiraj nas", + "aboutContactUsText": "Ako želiš s nama razgovarati prijeđi na Discord poslužitelj i kontaktiraj nas", "@aboutContactUsText": { "description": "Text for contact us section in the about dialog" }, @@ -825,5 +825,15 @@ "description": "Message returned if no exercises match the searched string" }, "aboutDonateTitle": "Doniraj", - "@aboutDonateTitle": {} + "@aboutDonateTitle": {}, + "aboutDonateText": "Pomogni projektu: kupi nam kavu, plati troškove poslužitelja i potiči nas u našem radu", + "@aboutDonateText": {}, + "settingsTitle": "Postavke", + "@settingsTitle": {}, + "settingsCacheTitle": "Predmemorija", + "@settingsCacheTitle": {}, + "settingsCacheDescription": "Izvrši predmemoriju", + "@settingsCacheDescription": {}, + "settingsCacheDeletedSnackbar": "Predmemorija je uspješno izbrisana", + "@settingsCacheDeletedSnackbar": {} } diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index f674d05d..9d36b087 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -759,5 +759,21 @@ "aboutMastodonText": "Projeyle ilgili güncellemeler ve haberler için bizi Mastodon'da takip edin", "@aboutMastodonText": { "description": "Text for the mastodon section in the about dialog" - } + }, + "settingsTitle": "Ayarlar", + "@settingsTitle": {}, + "settingsCacheTitle": "Önbellek", + "@settingsCacheTitle": {}, + "noMatchingExerciseFound": "Eşleşen egzersiz bulunamadı", + "@noMatchingExerciseFound": { + "description": "Message returned if no exercises match the searched string" + }, + "aboutDonateTitle": "Bağış yap", + "@aboutDonateTitle": {}, + "aboutDonateText": "Projeye yardımcı olmak, sunucu masraflarını karşılamak ve geliştirmeyi desteklemek için bağış yapın", + "@aboutDonateText": {}, + "settingsCacheDescription": "Egzersiz önbelleği", + "@settingsCacheDescription": {}, + "settingsCacheDeletedSnackbar": "Önbellek başarıyla temizlendi", + "@settingsCacheDeletedSnackbar": {} } diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index 2643976a..04957f7a 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -83,8 +83,7 @@ class AuthProvider with ChangeNotifier { /// Server application version Future setServerVersion() async { final response = await client.get(makeUri(serverUrl!, SERVER_VERSION_URL)); - final responseData = json.decode(response.body); - serverVersion = responseData; + serverVersion = json.decode(response.body); } Future initData(String serverUrl) async { @@ -115,41 +114,39 @@ class AuthProvider with ChangeNotifier { } /// Registers a new user - Future> register( - {required String username, - required String password, - required String email, - required String serverUrl}) async { + Future> register({ + required String username, + required String password, + required String email, + required String serverUrl, + String locale = 'en', + }) async { // Register - try { - final Map data = {'username': username, 'password': password}; - if (email != '') { - data['email'] = email; - } - final response = await client.post( - makeUri(serverUrl, REGISTRATION_URL), - headers: { - HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', - HttpHeaders.authorizationHeader: 'Token ${metadata[MANIFEST_KEY_API]}', - HttpHeaders.userAgentHeader: getAppNameHeader(), - }, - body: json.encode(data), - ); - final responseData = json.decode(response.body); - - if (response.statusCode >= 400) { - throw WgerHttpException(responseData); - } - - // If update is required don't log in user - if (await applicationUpdateRequired()) { - return {'action': LoginActions.update}; - } - - return login(username, password, serverUrl); - } catch (error) { - rethrow; + final Map data = {'username': username, 'password': password}; + if (email != '') { + data['email'] = email; } + final response = await client.post( + makeUri(serverUrl, REGISTRATION_URL), + headers: { + HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', + HttpHeaders.authorizationHeader: 'Token ${metadata[MANIFEST_KEY_API]}', + HttpHeaders.userAgentHeader: getAppNameHeader(), + HttpHeaders.acceptLanguageHeader: locale + }, + body: json.encode(data), + ); + + if (response.statusCode >= 400) { + throw WgerHttpException(response.body); + } + + // If update is required don't log in user + if (await applicationUpdateRequired()) { + return {'action': LoginActions.update}; + } + + return login(username, password, serverUrl); } /// Authenticates a user @@ -160,48 +157,44 @@ class AuthProvider with ChangeNotifier { ) async { await logout(shouldNotify: false); - try { - final response = await client.post( - makeUri(serverUrl, LOGIN_URL), - headers: { - HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', - HttpHeaders.userAgentHeader: getAppNameHeader(), - }, - body: json.encode({'username': username, 'password': password}), - ); - final responseData = json.decode(response.body); + final response = await client.post( + makeUri(serverUrl, LOGIN_URL), + headers: { + HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', + HttpHeaders.userAgentHeader: getAppNameHeader(), + }, + body: json.encode({'username': username, 'password': password}), + ); + final responseData = json.decode(response.body); - if (response.statusCode >= 400) { - throw WgerHttpException(responseData); - } - - await initData(serverUrl); - - // If update is required don't log in user - if (await applicationUpdateRequired()) { - return {'action': LoginActions.update}; - } - - // Log user in - token = responseData['token']; - notifyListeners(); - - // store login data in shared preferences - final prefs = await SharedPreferences.getInstance(); - final userData = json.encode({ - 'token': token, - 'serverUrl': this.serverUrl, - }); - final serverData = json.encode({ - 'serverUrl': this.serverUrl, - }); - - prefs.setString('userData', userData); - prefs.setString('lastServer', serverData); - return {'action': LoginActions.proceed}; - } catch (error) { - rethrow; + if (response.statusCode >= 400) { + throw WgerHttpException(response.body); } + + await initData(serverUrl); + + // If update is required don't log in user + if (await applicationUpdateRequired()) { + return {'action': LoginActions.update}; + } + + // Log user in + token = responseData['token']; + notifyListeners(); + + // store login data in shared preferences + final prefs = await SharedPreferences.getInstance(); + final userData = json.encode({ + 'token': token, + 'serverUrl': this.serverUrl, + }); + final serverData = json.encode({ + 'serverUrl': this.serverUrl, + }); + + prefs.setString('userData', userData); + prefs.setString('lastServer', serverData); + return {'action': LoginActions.proceed}; } /// Loads the last server URL from which the user successfully logged in diff --git a/lib/providers/base_provider.dart b/lib/providers/base_provider.dart index 7be7856e..e729e8cd 100644 --- a/lib/providers/base_provider.dart +++ b/lib/providers/base_provider.dart @@ -37,7 +37,7 @@ class WgerBaseProvider { this.client = client ?? http.Client(); } - Map getDefaultHeaders({includeAuth = false}) { + Map getDefaultHeaders({bool includeAuth = false}) { final out = { HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', HttpHeaders.userAgentHeader: auth.getAppNameHeader(), diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart index 92df4713..077e74cc 100644 --- a/lib/screens/auth_screen.dart +++ b/lib/screens/auth_screen.dart @@ -177,16 +177,21 @@ class _AuthCardState extends State { // Login existing user late Map res; if (_authMode == AuthMode.Login) { - res = await Provider.of(context, listen: false) - .login(_authData['username']!, _authData['password']!, _authData['serverUrl']!); + res = await Provider.of(context, listen: false).login( + _authData['username']!, + _authData['password']!, + _authData['serverUrl']!, + ); // Register new user } else { res = await Provider.of(context, listen: false).register( - username: _authData['username']!, - password: _authData['password']!, - email: _authData['email']!, - serverUrl: _authData['serverUrl']!); + username: _authData['username']!, + password: _authData['password']!, + email: _authData['email']!, + serverUrl: _authData['serverUrl']!, + locale: Localizations.localeOf(context).languageCode, + ); } // Check if update is required else continue normally diff --git a/pubspec.lock b/pubspec.lock index 438a64e0..75e046e5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -101,10 +101,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.4.8" build_runner_core: dependency: transitive description: @@ -301,18 +301,18 @@ packages: dependency: "direct main" description: name: drift - sha256: "05363b695885c72036ed5c76287125bfc6f1deda20cb3aa044a09fe22792f81b" + sha256: b50a8342c6ddf05be53bda1d246404cbad101b64dc73e8d6d1ac1090d119b4e2 url: "https://pub.dev" source: hosted - version: "2.14.1" + version: "2.15.0" drift_dev: dependency: "direct dev" description: name: drift_dev - sha256: "50c14b8248d133d36b41c1b08ceed138be869b5b98ccaf3af16c9b88c7adc54e" + sha256: c037d9431b6f8dc633652b1469e5f53aaec6e4eb405ed29dd232fa888ef10d88 url: "https://pub.dev" source: hosted - version: "2.14.1" + version: "2.15.0" equatable: dependency: "direct main" description: @@ -627,10 +627,10 @@ packages: dependency: "direct main" description: name: http - sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" http_multi_server: dependency: transitive description: @@ -1189,18 +1189,18 @@ packages: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: "3e3583b77cf888a68eae2e49ee4f025f66b86623ef0d83c297c8d903daa14871" + sha256: "90963b515721d6a71e96f438175cf43c979493ed14822860a300b69694c74eb6" url: "https://pub.dev" source: hosted - version: "0.5.18" + version: "0.5.19+1" sqlparser: dependency: transitive description: name: sqlparser - sha256: "877fcefabb725d120e31f54fa669a98c002db0feeaca6cea5354543f03b5e906" + sha256: dc384bb1f56d1384ce078edb5ff8247976abdab79d0c83e437210c85f06ecb61 url: "https://pub.dev" source: hosted - version: "0.33.0" + version: "0.34.0" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f168e86e..b31a6cdd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: flutter_html: ^3.0.0-beta.2 flutter_typeahead: ^4.8.0 font_awesome_flutter: ^10.4.0 - http: ^1.1.2 + http: ^1.2.0 image_picker: ^1.0.6 intl: ^0.18.1 json_annotation: ^4.8.1 @@ -59,10 +59,10 @@ dependencies: flutter_svg: ^2.0.5 fl_chart: ^0.66.0 flutter_zxing: ^1.5.2 - drift: ^2.13.1 + drift: ^2.15.0 path: ^1.8.3 path_provider: ^2.1.1 - sqlite3_flutter_libs: ^0.5.18 + sqlite3_flutter_libs: ^0.5.19+1 get_it: ^7.6.4 flex_seed_scheme: ^1.4.0 flex_color_scheme: ^7.3.1 @@ -73,13 +73,13 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - build_runner: ^2.3.3 + build_runner: ^2.4.8 json_serializable: ^6.7.1 mockito: ^5.4.4 network_image_mock: ^2.1.1 flutter_lints: ^3.0.1 cider: ^0.2.4 - drift_dev: ^2.13.1 + drift_dev: ^2.15.0 freezed: ^2.4.5 # For information on the generic Dart part of this file, see the diff --git a/test/auth/auth_provider_test.dart b/test/auth/auth_provider_test.dart index 93c280f6..40c5d77b 100644 --- a/test/auth/auth_provider_test.dart +++ b/test/auth/auth_provider_test.dart @@ -26,7 +26,6 @@ void main() { group('min application version check', () { test('app version higher than min version', () async { // arrange - when(mockClient.get(tVersionUri)).thenAnswer((_) => Future(() => Response('"1.2.0"', 200))); final updateNeeded = await authProvider.applicationUpdateRequired('1.3.0', testMetadata); diff --git a/test/auth/auth_screen_test.dart b/test/auth/auth_screen_test.dart index b5b80433..50647673 100644 --- a/test/auth/auth_screen_test.dart +++ b/test/auth/auth_screen_test.dart @@ -16,87 +16,228 @@ * along with this program. If not, see . */ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/http.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:wger/providers/auth.dart'; import 'package:wger/screens/auth_screen.dart'; +import 'auth_screen_test.mocks.dart'; + +@GenerateMocks([http.Client]) void main() { - testWidgets('Test the widgets on the auth screen, login mode', (WidgetTester tester) async { - // Wrap screen in material app so that the media query gets a context - await tester.pumpWidget( - MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (ctx) => AuthProvider(), - ), - ], - child: Consumer( - builder: (ctx, auth, _) => MaterialApp( - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - locale: const Locale('en'), - home: AuthScreen(), - ), + late AuthProvider authProvider; + late MockClient mockClient; + + final Uri tRegistration = Uri( + scheme: 'https', + host: 'wger.de', + path: 'api/v2/register/', + ); + + final Uri tLogin = Uri( + scheme: 'https', + host: 'wger.de', + path: 'api/v2/login/', + ); + + final responseLoginOk = {'token': 'b01c44d3e3e016a615d2f82b16d31f8b924fb936'}; + + final responseRegistrationOk = { + 'message': 'api user successfully registered', + 'token': 'b01c44d3e3e016a615d2f82b16d31f8b924fb936' + }; + + MultiProvider getWidget() { + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (ctx) => authProvider), + ], + child: Consumer( + builder: (ctx, auth, _) => MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + locale: const Locale('en'), + home: AuthScreen(), ), ), ); -/* - Provider( - create: (_) => Auth(), - // we use `builder` to obtain a new `BuildContext` that has access to the provider - builder: (context) { - // No longer throws - return Text(''), - } + } + + setUp(() { + mockClient = MockClient(); + authProvider = AuthProvider(mockClient, false); + authProvider.serverUrl = 'https://wger.de'; + + SharedPreferences.setMockInitialValues({}); + PackageInfo.setMockInitialValues( + appName: 'wger', + packageName: 'com.example.example', + version: '1.2.3', + buildNumber: '2', + buildSignature: 'buildSignature', ); - */ + when(mockClient.post( + tLogin, + headers: anyNamed('headers'), + body: anyNamed('body'), + )).thenAnswer((_) => Future(() => Response(json.encode(responseLoginOk), 200))); - Consumer( - builder: (ctx, auth, _) => MaterialApp( - builder: (ctx, authResultSnapshot) => AuthScreen(), - ), - ); - await tester.pumpAndSettle(); - expect(find.text('WGER'), findsOneWidget); + when(mockClient.get( + any, + )).thenAnswer((_) => Future(() => Response('"1.2.3.4"', 200))); - // Verify that the correct buttons and input fields are shown: login - expect(find.text('Register now'), findsOneWidget); - expect(find.text('LOGIN INSTEAD'), findsNothing); + when(mockClient.post( + tRegistration, + headers: anyNamed('headers'), + body: anyNamed('body'), + )).thenAnswer((_) => Future(() => Response(json.encode(responseRegistrationOk), 201))); + }); - // Check that the correct widgets are shown - expect(find.byKey(const Key('inputUsername')), findsOneWidget); - expect(find.byKey(const Key('inputEmail')), findsNothing); - expect(find.byKey(const Key('inputPassword')), findsOneWidget); - expect(find.byKey(const Key('inputServer')), findsNothing); - expect(find.byKey(const Key('inputPassword2')), findsNothing); - expect(find.byKey(const Key('actionButton')), findsOneWidget); - expect(find.byKey(const Key('toggleActionButton')), findsOneWidget); - expect(find.byKey(const Key('toggleCustomServerButton')), findsOneWidget); - }, skip: true); // TODO(x): skipped because of technical problems: - // either the provider wasn't found or, if the call was removed, the - // localization data could not be loaded... + group('Login mode', () { + testWidgets('Login smoke test', (WidgetTester tester) async { + // Act + await tester.pumpWidget(getWidget()); - testWidgets('Test the widgets on the auth screen, registration', (WidgetTester tester) async { - // Wrap screen in material app so that the media query gets a context - await tester.pumpWidget(MaterialApp(home: AuthScreen())); - await tester.tap(find.byKey(const Key('toggleActionButton'))); + // Assert + expect(find.text('wger'), findsOneWidget); - // Rebuild the widget after the state has changed. - await tester.pump(); - expect(find.text('Register now'), findsNothing); - expect(find.text('LOGIN INSTEAD'), findsOneWidget); + expect(find.textContaining("Don't have an account?"), findsOneWidget); + expect(find.textContaining('Already have an account?'), findsNothing); - // Check that the correct widgets are shown - expect(find.byKey(const Key('inputUsername')), findsOneWidget); - expect(find.byKey(const Key('inputEmail')), findsOneWidget); - expect(find.byKey(const Key('inputPassword')), findsOneWidget); - expect(find.byKey(const Key('inputServer')), findsOneWidget); - expect(find.byKey(const Key('inputPassword2')), findsOneWidget); - expect(find.byKey(const Key('actionButton')), findsOneWidget); - expect(find.byKey(const Key('toggleActionButton')), findsOneWidget); - }, skip: true); + expect(find.byKey(const Key('inputUsername')), findsOneWidget); + expect(find.byKey(const Key('inputEmail')), findsNothing); + expect(find.byKey(const Key('inputPassword')), findsOneWidget); + expect(find.byKey(const Key('inputServer')), findsNothing); + expect(find.byKey(const Key('inputPassword2')), findsNothing); + expect(find.byKey(const Key('actionButton')), findsOneWidget); + expect(find.byKey(const Key('toggleActionButton')), findsOneWidget); + expect(find.byKey(const Key('toggleCustomServerButton')), findsOneWidget); + }); + + testWidgets('Tests the login - happy path', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(getWidget()); + + // Act + await tester.enterText(find.byKey(const Key('inputUsername')), 'testuser'); + await tester.enterText(find.byKey(const Key('inputPassword')), '123456789'); + await tester.tap(find.byKey(const Key('actionButton'))); + await tester.pumpAndSettle(); + + // Assert + expect(find.textContaining('An Error Occurred'), findsNothing); + verify(mockClient.get(any)); + verify(mockClient.post( + tLogin, + headers: anyNamed('headers'), + body: json.encode({'username': 'testuser', 'password': '123456789'}), + )); + }); + }); + + group('Registration mode', () { + testWidgets('Registration smoke test', (WidgetTester tester) async { + // Arrange + await tester.binding.setSurfaceSize(const Size(1080, 1920)); + tester.view.devicePixelRatio = 1.0; + await tester.pumpWidget(getWidget()); + + // Act + await tester.tap(find.byKey(const Key('toggleActionButton'))); + await tester.pump(); + + // Assert + expect(find.textContaining("Don't have an account?"), findsNothing); + expect(find.textContaining('Already have an account?'), findsOneWidget); + + expect(find.byKey(const Key('inputUsername')), findsOneWidget); + expect(find.byKey(const Key('inputEmail')), findsOneWidget); + expect(find.byKey(const Key('inputPassword')), findsOneWidget); + expect(find.byKey(const Key('inputServer')), findsNothing); + expect(find.byKey(const Key('inputPassword2')), findsOneWidget); + expect(find.byKey(const Key('actionButton')), findsOneWidget); + expect(find.byKey(const Key('toggleActionButton')), findsOneWidget); + + // Act - show custom server + await tester.tap(find.byKey(const Key('toggleCustomServerButton'))); + await tester.pump(); + expect(find.byKey(const Key('inputServer')), findsOneWidget); + }); + + testWidgets('Tests the registration - happy path', (WidgetTester tester) async { + // Arrange + await tester.binding.setSurfaceSize(const Size(1080, 1920)); + tester.view.devicePixelRatio = 1.0; + await tester.pumpWidget(getWidget()); + + // Act + await tester.tap(find.byKey(const Key('toggleActionButton'))); + await tester.enterText(find.byKey(const Key('inputUsername')), 'testuser'); + await tester.enterText(find.byKey(const Key('inputPassword')), '123456789'); + await tester.enterText(find.byKey(const Key('inputPassword2')), '123456789'); + await tester.tap(find.byKey(const Key('actionButton'))); + await tester.pumpAndSettle(); + + // Assert + expect(find.textContaining('An Error Occurred'), findsNothing); + verify(mockClient.post( + tRegistration, + headers: anyNamed('headers'), + body: json.encode({'username': 'testuser', 'password': '123456789'}), + )); + }); + + testWidgets('Tests the registration - password problems', (WidgetTester tester) async { + // Arrange + await tester.binding.setSurfaceSize(const Size(1080, 1920)); + tester.view.devicePixelRatio = 1.0; + final response = { + 'username': [ + 'This field must be unique.', + ], + 'password': [ + 'This password is too common.', + 'This password is entirely numeric.', + ] + }; + + when(mockClient.post( + tRegistration, + headers: anyNamed('headers'), + body: anyNamed('body'), + )).thenAnswer((_) => Future(() => Response(json.encode(response), 400))); + await tester.pumpWidget(getWidget()); + + // Act + await tester.tap(find.byKey(const Key('toggleActionButton'))); + await tester.enterText(find.byKey(const Key('inputUsername')), 'testuser'); + await tester.enterText(find.byKey(const Key('inputPassword')), '123456789'); + await tester.enterText(find.byKey(const Key('inputPassword2')), '123456789'); + await tester.tap(find.byKey(const Key('actionButton'))); + await tester.pumpAndSettle(); + + // Assert + expect(find.textContaining('An Error Occurred'), findsOne); + expect(find.textContaining('This password is too common'), findsOne); + expect(find.textContaining('This password is entirely numeric'), findsOne); + expect(find.textContaining('This field must be unique'), findsOne); + + verify(mockClient.post( + tRegistration, + headers: anyNamed('headers'), + body: json.encode({'username': 'testuser', 'password': '123456789'}), + )); + }); + }); } diff --git a/test/auth/auth_screen_test.mocks.dart b/test/auth/auth_screen_test.mocks.dart new file mode 100644 index 00000000..3b46b01e --- /dev/null +++ b/test/auth/auth_screen_test.mocks.dart @@ -0,0 +1,279 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in wger/test/auth/auth_screen_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; +import 'dart:convert' as _i4; +import 'dart:typed_data' as _i6; + +import 'package:http/http.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i5; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeStreamedResponse_1 extends _i1.SmartFake implements _i2.StreamedResponse { + _FakeStreamedResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [Client]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockClient extends _i1.Mock implements _i2.Client { + MockClient() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.Response> head( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> get( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> post( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> put( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> patch( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> delete( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future read( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future.value(_i5.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future); + + @override + _i3.Future<_i6.Uint8List> readBytes( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #readBytes, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + ) as _i3.Future<_i6.Uint8List>); + + @override + _i3.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => (super.noSuchMethod( + Invocation.method( + #send, + [request], + ), + returnValue: _i3.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( + this, + Invocation.method( + #send, + [request], + ), + )), + ) as _i3.Future<_i2.StreamedResponse>); + + @override + void close() => super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/test/exercises/model_exercise_test.dart b/test/exercises/model_exercise_test.dart index f3ef3237..4a06de28 100644 --- a/test/exercises/model_exercise_test.dart +++ b/test/exercises/model_exercise_test.dart @@ -40,8 +40,8 @@ void main() { expect(exercise.uuid, '1b020b3a-3732-4c7e-92fd-a0cec90ed69b'); expect(exercise.categoryId, 10); expect(exercise.variationId, 25); - expect(exercise.authors, ["Foo Bar"]); - expect(exercise.authorsGlobal, ["Foo Bar", "tester McTestface", "Mr. X"]); + expect(exercise.authors, ['Foo Bar']); + expect(exercise.authorsGlobal, ['Foo Bar', 'tester McTestface', 'Mr. X']); expect(exercise.equipment.map((e) => e.name), ['Kettlebell']); expect(exercise.muscles.map((e) => e.name), [ 'Biceps femoris', diff --git a/test/gallery/gallery_form_test.mocks.dart b/test/gallery/gallery_form_test.mocks.dart index e6a42a1d..d54237c8 100644 --- a/test/gallery/gallery_form_test.mocks.dart +++ b/test/gallery/gallery_form_test.mocks.dart @@ -195,7 +195,7 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { ) as _i6.Future); @override - Map getDefaultHeaders({dynamic includeAuth = false}) => (super.noSuchMethod( + Map getDefaultHeaders({bool? includeAuth = false}) => (super.noSuchMethod( Invocation.method( #getDefaultHeaders, [], diff --git a/test/gallery/gallery_screen_test.mocks.dart b/test/gallery/gallery_screen_test.mocks.dart index bea4448f..fe6aebc6 100644 --- a/test/gallery/gallery_screen_test.mocks.dart +++ b/test/gallery/gallery_screen_test.mocks.dart @@ -195,7 +195,7 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { ) as _i6.Future); @override - Map getDefaultHeaders({dynamic includeAuth = false}) => (super.noSuchMethod( + Map getDefaultHeaders({bool? includeAuth = false}) => (super.noSuchMethod( Invocation.method( #getDefaultHeaders, [], diff --git a/test/measurements/measurement_provider_test.mocks.dart b/test/measurements/measurement_provider_test.mocks.dart index 280d6390..01aadd25 100644 --- a/test/measurements/measurement_provider_test.mocks.dart +++ b/test/measurements/measurement_provider_test.mocks.dart @@ -108,7 +108,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({dynamic includeAuth = false}) => (super.noSuchMethod( + Map getDefaultHeaders({bool? includeAuth = false}) => (super.noSuchMethod( Invocation.method( #getDefaultHeaders, [], diff --git a/test/nutrition/nutritional_plan_screen_test.mocks.dart b/test/nutrition/nutritional_plan_screen_test.mocks.dart index 4711f7f2..23808408 100644 --- a/test/nutrition/nutritional_plan_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plan_screen_test.mocks.dart @@ -123,7 +123,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({dynamic includeAuth = false}) => (super.noSuchMethod( + Map getDefaultHeaders({bool? includeAuth = false}) => (super.noSuchMethod( Invocation.method( #getDefaultHeaders, [], @@ -395,6 +395,7 @@ class MockAuthProvider extends _i1.Mock implements _i2.AuthProvider { required String? password, required String? email, required String? serverUrl, + String? locale = r'en', }) => (super.noSuchMethod( Invocation.method( @@ -405,6 +406,7 @@ class MockAuthProvider extends _i1.Mock implements _i2.AuthProvider { #password: password, #email: email, #serverUrl: serverUrl, + #locale: locale, }, ), returnValue: _i5.Future>.value({}), diff --git a/test/nutrition/nutritional_plans_screen_test.mocks.dart b/test/nutrition/nutritional_plans_screen_test.mocks.dart index 53513e7f..5c57447a 100644 --- a/test/nutrition/nutritional_plans_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plans_screen_test.mocks.dart @@ -234,6 +234,7 @@ class MockAuthProvider extends _i1.Mock implements _i3.AuthProvider { required String? password, required String? email, required String? serverUrl, + String? locale = r'en', }) => (super.noSuchMethod( Invocation.method( @@ -244,6 +245,7 @@ class MockAuthProvider extends _i1.Mock implements _i3.AuthProvider { #password: password, #email: email, #serverUrl: serverUrl, + #locale: locale, }, ), returnValue: _i5.Future>.value({}), @@ -399,7 +401,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i8.WgerBaseProvider { ); @override - Map getDefaultHeaders({dynamic includeAuth = false}) => (super.noSuchMethod( + Map getDefaultHeaders({bool? includeAuth = false}) => (super.noSuchMethod( Invocation.method( #getDefaultHeaders, [], diff --git a/test/user/provider_test.mocks.dart b/test/user/provider_test.mocks.dart index c06d47a1..4b2572d9 100644 --- a/test/user/provider_test.mocks.dart +++ b/test/user/provider_test.mocks.dart @@ -108,7 +108,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({dynamic includeAuth = false}) => (super.noSuchMethod( + Map getDefaultHeaders({bool? includeAuth = false}) => (super.noSuchMethod( Invocation.method( #getDefaultHeaders, [], diff --git a/test/weight/weight_provider_test.mocks.dart b/test/weight/weight_provider_test.mocks.dart index 86083803..ac0e017b 100644 --- a/test/weight/weight_provider_test.mocks.dart +++ b/test/weight/weight_provider_test.mocks.dart @@ -108,7 +108,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({dynamic includeAuth = false}) => (super.noSuchMethod( + Map getDefaultHeaders({bool? includeAuth = false}) => (super.noSuchMethod( Invocation.method( #getDefaultHeaders, [], diff --git a/test/workout/gym_mode_screen_test.mocks.dart b/test/workout/gym_mode_screen_test.mocks.dart index f4aa799f..691eebef 100644 --- a/test/workout/gym_mode_screen_test.mocks.dart +++ b/test/workout/gym_mode_screen_test.mocks.dart @@ -186,7 +186,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({dynamic includeAuth = false}) => (super.noSuchMethod( + Map getDefaultHeaders({bool? includeAuth = false}) => (super.noSuchMethod( Invocation.method( #getDefaultHeaders, [], diff --git a/test/workout/workout_plan_screen_test.mocks.dart b/test/workout/workout_plan_screen_test.mocks.dart index 7a89d992..a4f438a6 100644 --- a/test/workout/workout_plan_screen_test.mocks.dart +++ b/test/workout/workout_plan_screen_test.mocks.dart @@ -108,7 +108,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({dynamic includeAuth = false}) => (super.noSuchMethod( + Map getDefaultHeaders({bool? includeAuth = false}) => (super.noSuchMethod( Invocation.method( #getDefaultHeaders, [], diff --git a/test/workout/workout_plans_screen_test.mocks.dart b/test/workout/workout_plans_screen_test.mocks.dart index 0756f351..29738eb6 100644 --- a/test/workout/workout_plans_screen_test.mocks.dart +++ b/test/workout/workout_plans_screen_test.mocks.dart @@ -108,7 +108,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({dynamic includeAuth = false}) => (super.noSuchMethod( + Map getDefaultHeaders({bool? includeAuth = false}) => (super.noSuchMethod( Invocation.method( #getDefaultHeaders, [], diff --git a/test/workout/workout_provider_test.mocks.dart b/test/workout/workout_provider_test.mocks.dart index 8309f4ec..3b79f989 100644 --- a/test/workout/workout_provider_test.mocks.dart +++ b/test/workout/workout_provider_test.mocks.dart @@ -108,7 +108,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({dynamic includeAuth = false}) => (super.noSuchMethod( + Map getDefaultHeaders({bool? includeAuth = false}) => (super.noSuchMethod( Invocation.method( #getDefaultHeaders, [], diff --git a/test/workout/workout_set_form_test.mocks.dart b/test/workout/workout_set_form_test.mocks.dart index 792776b9..f0ab90a7 100644 --- a/test/workout/workout_set_form_test.mocks.dart +++ b/test/workout/workout_set_form_test.mocks.dart @@ -755,7 +755,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { ); @override - Map getDefaultHeaders({dynamic includeAuth = false}) => (super.noSuchMethod( + Map getDefaultHeaders({bool? includeAuth = false}) => (super.noSuchMethod( Invocation.method( #getDefaultHeaders, [], diff --git a/test_data/screenshots_exercises.dart b/test_data/screenshots_exercises.dart index 40d580a1..3630108b 100644 --- a/test_data/screenshots_exercises.dart +++ b/test_data/screenshots_exercises.dart @@ -211,7 +211,8 @@ final benchPressDE = Translation( created: DateTime(2021, 1, 15), name: 'Bankdrücken LH', description: - '''

Lege dich auf die Bank, die Stange direkt über die Augen, die Knie etwas angewinkelt und die Füße fest auf dem Boden. Greife die Stange breit und lasse sie langsam und kontrolliert runter, dabei sollte die Stange kurz auf Brustwarzenhöhe den Körper berühren. Dann das Gewicht wieder hochdrücken bis die Arme durchgestreckt sind.

+ ''' +

Lege dich auf die Bank, die Stange direkt über die Augen, die Knie etwas angewinkelt und die Füße fest auf dem Boden. Greife die Stange breit und lasse sie langsam und kontrolliert runter, dabei sollte die Stange kurz auf Brustwarzenhöhe den Körper berühren. Dann das Gewicht wieder hochdrücken bis die Arme durchgestreckt sind.

Bei hohem Gewicht, empfielt sich natürlich einen Spotter zu haben, der einen hilft falls man die Stange nicht alleine hochdrücken kann.

Mit der Breite des Griffs kann außerdem kontrolliert werden, welcher Bereich der Brust stärker belastet wird:

    @@ -228,7 +229,8 @@ final benchPressEN = Translation( created: DateTime(2021, 1, 15), name: 'Bench Press', description: - '''

    Lay down on a bench, the bar should be directly above your eyes, the knees are somewhat angled and the feet are firmly on the floor. Concentrate, breath deeply and grab the bar more than shoulder wide. Bring it slowly down till it briefly touches your chest at the height of your nipples. Push the bar up.

    + ''' +

    Lay down on a bench, the bar should be directly above your eyes, the knees are somewhat angled and the feet are firmly on the floor. Concentrate, breath deeply and grab the bar more than shoulder wide. Bring it slowly down till it briefly touches your chest at the height of your nipples. Push the bar up.

    If you train with a high weight it is advisable to have a spotter that can help you up if you can't lift the weight on your own.

    With the width of the grip you can also control which part of the chest is trained more:

      @@ -257,7 +259,8 @@ final deadLiftEN = Translation( created: DateTime(2021, 1, 15), name: 'Deadlifts', description: - '''

      Stand firmly, with your feet slightly more than shoulder wide apart. Stand directly behind the bar where it should barely touch your shin, your feet pointing a bit out. Bend down with a straight back, the knees also pointing somewhat out. Grab the bar with a shoulder wide grip, one underhand, one reverse grip.

      + ''' +

      Stand firmly, with your feet slightly more than shoulder wide apart. Stand directly behind the bar where it should barely touch your shin, your feet pointing a bit out. Bend down with a straight back, the knees also pointing somewhat out. Grab the bar with a shoulder wide grip, one underhand, one reverse grip.

      Pull the weight up. At the highest point make a slight hollow back and pull the bar back. Hold 1 or 2 seconds that position. Go down, making sure the back is not bent. Once down you can either go back again as soon as the weights touch the floor, or make a pause, depending on the weight.

      ''', exerciseId: 184, language: tLanguage2, @@ -269,7 +272,8 @@ final deadLiftDE = Translation( created: DateTime(2021, 1, 15), name: 'Kreuzheben', description: - '''

      Stelle dich mit etwas mehr als schulterbreitem Stand vor der Stange, die Füße zeigen leicht nach außen, die Stange ist direkt darüber und sehr nahe am Schienbein. Beuge die Knie (zeigen ebenfalls etwas nach außen) und neige den Oberkörper (bleibt während der ganzen Übung gerade). Greife die Stange schulterbreit mit einem Unter- und einem Obergriff.

      + ''' +

      Stelle dich mit etwas mehr als schulterbreitem Stand vor der Stange, die Füße zeigen leicht nach außen, die Stange ist direkt darüber und sehr nahe am Schienbein. Beuge die Knie (zeigen ebenfalls etwas nach außen) und neige den Oberkörper (bleibt während der ganzen Übung gerade). Greife die Stange schulterbreit mit einem Unter- und einem Obergriff.

      Ziehe nun die Stange nach oben. An der höchsten Stelle mache ein leichtes Hohlkreuz und drücke die Schultern nach hinten. Gehe wieder runter, wobei du darauf achtest, dass der Rücken gerade bleibt und sich nicht krümmt. Du kannst unten angekommen eine kleine Pause einlegen oder sofort weitermachen.

      ''', exerciseId: 184, language: tLanguage1, @@ -281,7 +285,8 @@ final deadLiftPT = Translation( created: DateTime(2021, 1, 15), name: 'Levantamento terra', description: - '''Fique firme, com os pés ligeiramente mais afastados do que os ombros. Fique diretamente atrás da barra, onde ela mal deve tocar sua canela, com os pés apontando um pouco para fora. Curve-se com as costas retas, os joelhos também apontando um pouco para fora. Agarre a barra com uma pegada na largura dos ombros, uma pegada por baixo e uma pegada reversa. + ''' +Fique firme, com os pés ligeiramente mais afastados do que os ombros. Fique diretamente atrás da barra, onde ela mal deve tocar sua canela, com os pés apontando um pouco para fora. Curve-se com as costas retas, os joelhos também apontando um pouco para fora. Agarre a barra com uma pegada na largura dos ombros, uma pegada por baixo e uma pegada reversa. Puxe o peso para cima. No ponto mais alto, faça uma leve depressão para trás e puxe a barra para trás. Segure 1 ou 2 segundos nessa posição. Desça, certificando-se de que as costas não estão dobradas. Depois de descer, você pode voltar assim que os pesos tocarem o chão ou fazer uma pausa, dependendo do peso.''', exerciseId: 184, @@ -293,7 +298,8 @@ final deadLiftIT = Translation( uuid: '7c088d54-6732-4d5e-aae8-9be8d4a1f111', created: DateTime(2021, 1, 15), name: 'Stacco', - description: '''StaccoStacco + description: ''' +StaccoStacco Stacco''', exerciseId: 184, language: tLanguage13, @@ -373,7 +379,8 @@ final crunchesDE = Translation( created: DateTime(2021, 1, 15), name: 'Crunches', description: - '''

      Lege dich auf eine Matte mit angewinkelten Beinen. Die Füße werden irgendwie festgehalten (Partner, Lanhghantel, o.Ä.) und die Hände werden hinter dem Nacken verschränkt. Aus dieser Position führe den Oberkörper so weit nach oben, bis Kopf oder Ellenbogen die angewinkelten Beine berühren.

      + ''' +

      Lege dich auf eine Matte mit angewinkelten Beinen. Die Füße werden irgendwie festgehalten (Partner, Lanhghantel, o.Ä.) und die Hände werden hinter dem Nacken verschränkt. Aus dieser Position führe den Oberkörper so weit nach oben, bis Kopf oder Ellenbogen die angewinkelten Beine berühren.

      Es ist wichtig, dass dieser Vorgang mit einer rollenden Bewegung durchgeführt wird: die Wirbelsäule sollte sich Wirbel für Wirbel von der Matte lösen. Ein Hohlkreuz ist stets zu vermeiden.

      ''', exerciseId: 167, language: tLanguage1, @@ -500,7 +507,8 @@ final curlsEN = Translation( created: DateTime(2021, 1, 15), name: 'Biceps Curls With Dumbbell', description: - '''

      Hold two barbells, the arms are streched, the hands are on your side, the palms face inwards. Bend the arms and bring the weight with a fast movement up. At the same time, rotate your arms by 90 degrees at the very beginning of the movement. At the highest point, rotate a little the weights further outwards. Without a pause, bring them down, slowly.

      + ''' +

      Hold two barbells, the arms are streched, the hands are on your side, the palms face inwards. Bend the arms and bring the weight with a fast movement up. At the same time, rotate your arms by 90 degrees at the very beginning of the movement. At the highest point, rotate a little the weights further outwards. Without a pause, bring them down, slowly.

      Don't allow your body to swing during the exercise, all work is done by the biceps, which are the only mucles that should move (pay attention to the elbows).

      ''', exerciseId: 92, language: tLanguage2, @@ -512,7 +520,8 @@ final curlsDE = Translation( created: DateTime(2021, 1, 15), name: 'Bizeps KH-Curls', description: - '''

      Halte zwei Kurzhantel mit ausgestreckten Armen neben dem Körper, die Handflächen zeigen nach innen. Beuge die Arme und brige die Hanteln mit einer schnellen Bewegung nach oben wobei sie gleichzeitig um 90 Grad gedreht werden. Am höchsten Punkt kann man die Hanteln ganz leicht weiter nach außen drehen. Ohne Pause wird das Gewicht nun kontrolliert nach unten gebracht. Beachte, dass die Bewegung nach oben schneller ist als nach unten.

      + ''' +

      Halte zwei Kurzhantel mit ausgestreckten Armen neben dem Körper, die Handflächen zeigen nach innen. Beuge die Arme und brige die Hanteln mit einer schnellen Bewegung nach oben wobei sie gleichzeitig um 90 Grad gedreht werden. Am höchsten Punkt kann man die Hanteln ganz leicht weiter nach außen drehen. Ohne Pause wird das Gewicht nun kontrolliert nach unten gebracht. Beachte, dass die Bewegung nach oben schneller ist als nach unten.

      Während des Bewegungablaufs darf der Körper nicht mitschwingen. Die Ellenbogen bleiben dabei immer an der Stelle.

      ''', exerciseId: 92, language: tLanguage1, @@ -557,7 +566,8 @@ final raisesDE = Translation( uuid: '72e78f4d-65f7-4ddd-9247-cdc1e133fa80', created: DateTime(2021, 1, 15), name: 'Seitheben KH', - description: '''
        + description: ''' +
        1. Aufrechter Stand, Kopf nach vorne gerichtet, eine Kurzhantel in jeder Hand.
        2. Handflächen nach innen, in Richtung des Körpers, gerichtet.
        3. Hebe die Hanteln seitlich nach oben, ohne Schwung zu holen. Atme aus. Die Ellenbogen sind leicht gebeugt. Hebe die Kurzhanteln soweit, bis die Arme parallel zum Boden sind.