diff --git a/.github/actions/flutter-common/action.yml b/.github/actions/flutter-common/action.yml index ef4db6bb..ee3b35f9 100644 --- a/.github/actions/flutter-common/action.yml +++ b/.github/actions/flutter-common/action.yml @@ -9,7 +9,7 @@ runs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.3 + flutter-version: 3.38.6 cache: true - name: Install Flutter dependencies diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 4d9debf5..c72d8e6f 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -36,7 +36,7 @@ jobs: - name: Build APK run: flutter build apk --release - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-apk path: build/app/outputs/flutter-apk/app-release.apk @@ -67,7 +67,7 @@ jobs: - name: Build AAB run: flutter build appbundle --release - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-aab path: build/app/outputs/bundle/release/app-release.aab \ No newline at end of file diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index 4a2069d5..083677e1 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -31,7 +31,7 @@ jobs: cd build/ios/iphoneos zip -r Runner.app.zip Runner.app - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-ios path: build/ios/iphoneos/Runner.app.zip @@ -61,7 +61,7 @@ jobs: cd build/ios/archive zip -r Runner.xcarchive.zip Runner.xcarchive - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-ipa path: build/ios/archive/Runner.xcarchive.zip @@ -84,7 +84,7 @@ jobs: cd build/macos/Build/Products/Release zip -r wger.app.zip wger.app - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-macos path: build/macos/Build/Products/Release/wger.app.zip \ No newline at end of file diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index cb9f17b9..84e01dd5 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -39,7 +39,7 @@ jobs: sudo apt install -y pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev --no-install-recommends flutter build linux --release tar -zcvf linux-${{ matrix.platform }}.tar.gz build/linux/${{ matrix.platform }}/release/bundle - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-linux path: | @@ -62,7 +62,7 @@ jobs: - name: Bump version and update manifest run: | - git clone https://github.com/TheAppgineer/flatpak-flutter.git --branch 0.7.5 ../flatpak-flutter + git clone https://github.com/TheAppgineer/flatpak-flutter.git --branch 0.11.0 ../flatpak-flutter pip install -r ../flatpak-flutter/requirements.txt python bump-wger-version.py ${{ inputs.ref }} ../flatpak-flutter/flatpak-flutter.py --app-module wger flatpak-flutter.json diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 45a48257..56eb0947 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -25,7 +25,7 @@ jobs: - name: Build .exe run: flutter build windows --release - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-windows path: build\windows\x64\runner\Release\wger.exe \ No newline at end of file diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml index 0eb83b71..9afec0a3 100644 --- a/.github/workflows/make-release.yml +++ b/.github/workflows/make-release.yml @@ -67,7 +67,7 @@ jobs: ref: ${{ github.event.inputs.version }} - name: Download builds - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: path: /tmp/ @@ -115,7 +115,7 @@ jobs: # uses: ./.github/actions/flutter-common # # - name: Download builds - # uses: actions/download-artifact@v6 + # uses: actions/download-artifact@v7 # with: # path: /tmp/ # @@ -134,7 +134,7 @@ jobs: steps: - name: Download builds - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 - name: Make Github release uses: softprops/action-gh-release@v2 diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 61d7662b..d3fba326 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -32,7 +32,7 @@ jobs: flutter drive --driver=test_driver/screenshot_driver.dart --target=integration_test/make_screenshots_test.dart --dart-define=DEVICE_TYPE=iOSPhoneBig -d "$SIMULATOR" - name: Upload screenshots - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: screenshots-ios path: fastlane/metadata/ios/**/images/iPhone 6.9/*.png @@ -113,7 +113,7 @@ jobs: flutter drive --driver=test_driver/screenshot_driver.dart --target=integration_test/make_screenshots_test.dart --dart-define=DEVICE_TYPE=${{ matrix.device.device_type }} - name: Upload screenshots - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: screenshots-android-${{ matrix.device.folder }} path: fastlane/metadata/android/**/images/${{ matrix.device.folder }}/*.png @@ -129,7 +129,7 @@ jobs: - uses: actions/checkout@v6 - name: Download all screenshot artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: path: screenshots diff --git a/Gemfile.lock b/Gemfile.lock index 2fba95de..313cae7f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,8 +8,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1190.0) - aws-sdk-core (3.239.2) + aws-partitions (1.1203.0) + aws-sdk-core (3.241.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -17,18 +17,18 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.118.0) - aws-sdk-core (~> 3, >= 3.239.1) + aws-sdk-kms (1.120.0) + aws-sdk-core (~> 3, >= 3.241.3) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.206.0) - aws-sdk-core (~> 3, >= 3.234.0) + aws-sdk-s3 (1.211.0) + aws-sdk-core (~> 3, >= 3.241.3) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) - bigdecimal (3.3.1) + bigdecimal (4.0.1) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -61,7 +61,7 @@ GEM faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.1.1) + faraday-multipart (1.2.0) multipart-post (~> 2.0) faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) @@ -71,7 +71,7 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.0) - fastlane (2.229.1) + fastlane (2.230.0) CFPropertyList (>= 2.3, < 4.0.0) abbrev (~> 0.1.2) addressable (>= 2.8, < 3.0.0) @@ -100,6 +100,7 @@ GEM http-cookie (~> 1.0.5) json (< 3.0.0) jwt (>= 2.1.0, < 3) + logger (>= 1.6, < 2.0) mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) mutex_m (~> 0.3.0) @@ -164,22 +165,22 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.17.1) + json (2.18.0) jwt (2.10.2) base64 logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - multi_json (1.18.0) + multi_json (1.19.1) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) naturally (2.3.0) nkf (0.2.0) - optparse (0.8.0) + optparse (0.8.1) os (1.1.4) plist (3.7.2) - public_suffix (7.0.0) + public_suffix (7.0.2) rake (13.3.1) representable (3.2.0) declarative (< 0.1.0) diff --git a/analysis_options.yaml b/analysis_options.yaml index b7abc707..79c8d7ca 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -14,15 +14,16 @@ analyzer: # Allow self-reference to deprecated members (we do this because otherwise we have # to annotate every member in every test, assert, etc, when we deprecate something) deprecated_member_use_from_same_package: ignore - # Ignore analyzer hints for updating pubspecs when using Future or - # Stream and not importing dart:async - # Please see https://github.com/flutter/flutter/pull/24528 for details. - sdk_version_async_exported_from_core: ignore + plugins: + - riverpod_lint formatter: page_width: 100 trailing_commas: preserve +plugins: + riverpod_lint: 3.1.0 + linter: rules: # These rules are documented on and in the same order as diff --git a/flatpak/de.wger.flutter.metainfo.xml b/flatpak/de.wger.flutter.metainfo.xml index 7d45635c..9b0daf14 100755 --- a/flatpak/de.wger.flutter.metainfo.xml +++ b/flatpak/de.wger.flutter.metainfo.xml @@ -84,6 +84,24 @@ + + +

Bug fixes and improvements.

+
+ https://github.com/wger-project/flutter/releases/tag/1.9.5 +
+ + +

Bug fixes and improvements.

+
+ https://github.com/wger-project/flutter/releases/tag/1.9.4 +
+ + +

Bug fixes and improvements.

+
+ https://github.com/wger-project/flutter/releases/tag/1.9.3 +

Bug fixes and improvements.

diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 5447a46b..b05f5052 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -9,24 +9,28 @@ GEM public_suffix (>= 2.0.2, < 7.0) artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.3.0) - aws-partitions (1.1018.0) - aws-sdk-core (3.214.0) + aws-eventstream (1.4.0) + aws-partitions (1.1196.0) + aws-sdk-core (3.240.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) + base64 + bigdecimal jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.96.0) - aws-sdk-core (~> 3, >= 3.210.0) + logger + aws-sdk-kms (1.118.0) + aws-sdk-core (~> 3, >= 3.239.1) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.176.0) - aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-s3 (1.208.0) + aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.10.1) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) + bigdecimal (4.0.1) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -157,6 +161,7 @@ GEM json (2.9.0) jwt (2.9.3) base64 + logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) diff --git a/lib/core/exceptions/http_exception.dart b/lib/core/exceptions/http_exception.dart new file mode 100644 index 00000000..73cbc24c --- /dev/null +++ b/lib/core/exceptions/http_exception.dart @@ -0,0 +1,86 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2020 - 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart'; + +enum ErrorType { + json, + html, + text, +} + +const HTML_ERROR_KEY = 'html_error'; + +class WgerHttpException implements Exception { + Map errors = {}; + + /// The exception type. While the majority will be json, it is possible that + /// the server will return HTML, e.g. if there has been an internal server error + /// or similar. + late ErrorType type; + + /// Custom http exception + WgerHttpException(Response response) { + type = ErrorType.json; + final dynamic responseBody = response.body; + + final contentType = response.headers[HttpHeaders.contentTypeHeader]; + if ((contentType != null && contentType.contains('text/html')) || + responseBody.toString().contains('(); + } else if (type == ErrorType.html) { + errors = {HTML_ERROR_KEY: responseBody.toString()}; + } else { + errors = {'text_error': responseBody.toString()}; + } + } catch (e) { + errors = {'unknown_error': responseBody}; + } + } + } + + WgerHttpException.fromMap(Map map) : type = ErrorType.json { + errors = map; + } + + String get htmlError { + if (type != ErrorType.html) { + return ''; + } + + return errors[HTML_ERROR_KEY] ?? ''; + } + + @override + String toString() { + return 'WgerHttpException ($type): $errors'; + } +} diff --git a/lib/exceptions/no_result_exception.dart b/lib/core/exceptions/no_result_exception.dart similarity index 100% rename from lib/exceptions/no_result_exception.dart rename to lib/core/exceptions/no_result_exception.dart diff --git a/lib/exceptions/no_such_entry_exception.dart b/lib/core/exceptions/no_such_entry_exception.dart similarity index 100% rename from lib/exceptions/no_such_entry_exception.dart rename to lib/core/exceptions/no_such_entry_exception.dart diff --git a/lib/database/ingredients/ingredients_database.g.dart b/lib/database/ingredients/ingredients_database.g.dart index 7dc02ac3..44ebcde2 100644 --- a/lib/database/ingredients/ingredients_database.g.dart +++ b/lib/database/ingredients/ingredients_database.g.dart @@ -429,7 +429,10 @@ typedef $$IngredientsTableProcessedTableManager = $$IngredientsTableAnnotationComposer, $$IngredientsTableCreateCompanionBuilder, $$IngredientsTableUpdateCompanionBuilder, - (IngredientTable, BaseReferences<_$IngredientDatabase, $IngredientsTable, IngredientTable>), + ( + IngredientTable, + BaseReferences<_$IngredientDatabase, $IngredientsTable, IngredientTable>, + ), IngredientTable, PrefetchHooks Function() >; diff --git a/lib/exceptions/http_exception.dart b/lib/exceptions/http_exception.dart deleted file mode 100644 index 06cd2915..00000000 --- a/lib/exceptions/http_exception.dart +++ /dev/null @@ -1,48 +0,0 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team - * - * wger Workout Manager is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import 'dart:convert'; - -class WgerHttpException implements Exception { - Map errors = {}; - - /// Custom http exception. - /// Expects the response body of the REST call and will try to parse it to - /// JSON. Will use the response as-is if it fails. - WgerHttpException(dynamic responseBody) { - if (responseBody == null) { - errors = {'unknown_error': 'An unknown error occurred, no further information available'}; - } else { - try { - final response = json.decode(responseBody); - errors = (response is Map ? response : {'unknown_error': response}).cast(); - } catch (e) { - errors = {'unknown_error': responseBody}; - } - } - } - - WgerHttpException.fromMap(Map map) { - errors = map; - } - - @override - String toString() { - return errors.values.toList().join(', '); - } -} diff --git a/lib/helpers/errors.dart b/lib/helpers/errors.dart index 77d03a0a..139ef917 100644 --- a/lib/helpers/errors.dart +++ b/lib/helpers/errors.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -22,11 +22,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/main.dart'; import 'package:wger/models/workouts/log.dart'; @@ -50,16 +51,21 @@ void showHttpExceptionErrorDialog(WgerHttpException exception, {BuildContext? co return; } - final errorList = formatApiErrors(extractErrors(exception.errors)); - showDialog( context: dialogContext, builder: (ctx) => AlertDialog( title: Text(AppLocalizations.of(ctx).anErrorOccurred), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [...errorList], + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (exception.type == ErrorType.html) + ServerHtmlError(data: exception.htmlError) + else + ...formatApiErrors(extractErrors(exception.errors)), + ], + ), ), actions: [ TextButton( @@ -145,7 +151,10 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext tilePadding: EdgeInsets.zero, title: Text(i18n.errorViewDetails), children: [ - Text(issueErrorMessage, style: const TextStyle(fontWeight: FontWeight.bold)), + Text( + issueErrorMessage, + style: const TextStyle(fontWeight: FontWeight.bold), + ), Container( alignment: Alignment.topLeft, padding: const EdgeInsets.symmetric(vertical: 8.0), @@ -237,6 +246,31 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext ); } +/// A widget to render HTML errors returned by the server +/// +/// This is a simple wrapper around the `Html` Widget, with some light changes +/// to the style. +class ServerHtmlError extends StatelessWidget { + final logger = Logger('ServerHtml'); + final String data; + + ServerHtmlError({required this.data, super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Html( + data: data, + style: { + 'h1': Style(fontSize: FontSize(theme.textTheme.bodyLarge?.fontSize ?? 15)), + 'h2': Style(fontSize: FontSize(theme.textTheme.bodyMedium?.fontSize ?? 15)), + }, + doNotRenderTheseTags: const {'a'}, + ); + } +} + class CopyToClipboardButton extends StatelessWidget { final logger = Logger('CopyToClipboardButton'); final String text; @@ -423,38 +457,26 @@ class FormHttpErrorsWidget extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( constraints: const BoxConstraints(maxHeight: 250), + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.error, width: 1), + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.all(10), child: SingleChildScrollView( child: Column( children: [ - Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error), - ...formatApiErrors( - extractErrors(exception.errors), - color: Theme.of(context).colorScheme.error, - ), - ], - ), - ), - ); - } -} - -class GeneralErrorsWidget extends StatelessWidget { - final String? title; - final List widgets; - - const GeneralErrorsWidget(this.widgets, {this.title, super.key}); - - @override - Widget build(BuildContext context) { - return Container( - constraints: const BoxConstraints(maxHeight: 250), - child: SingleChildScrollView( - child: Column( - children: [ - Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error), - ...formatTextErrors(widgets, title: title, color: Theme.of(context).colorScheme.error), + Icon(Icons.error_outline, color: theme.colorScheme.error), + if (exception.type == ErrorType.html) + ServerHtmlError(data: exception.htmlError) + else + ...formatApiErrors( + extractErrors(exception.errors), + color: theme.colorScheme.error, + ), ], ), ), diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f768dcd6..5c98ddd0 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -705,7 +705,7 @@ "@whatVariationsExist": {}, "previous": "Vorherige", "@previous": {}, - "next": "Nächste", + "next": "Weiter", "@next": {}, "swiss_ball": "Gymnastikball", "@swiss_ball": {}, @@ -1127,5 +1127,65 @@ "endWorkout": "Training beenden", "@endWorkout": {}, "dayTypeCustom": "personalisierte", - "@dayTypeCustom": {} + "@dayTypeCustom": {}, + "dayTypeTabata": "Tabata", + "@dayTypeTabata": {}, + "impressionGood": "Gut", + "@impressionGood": {}, + "impressionNeutral": "Neutral", + "@impressionNeutral": {}, + "impressionBad": "Schlecht", + "@impressionBad": {}, + "gymModeShowExercises": "Übersichtsseiten der Übungen anzeigen", + "@gymModeShowExercises": {}, + "gymModeShowTimer": "Timer zwischen Sätzen anzeigen", + "@gymModeShowTimer": {}, + "gymModeTimerType": "Timer-Typ", + "@gymModeTimerType": {}, + "gymModeTimerTypeHelText": "Wenn ein Satz eine Pausenzeit hat, wird immer ein Countdown genutzt.", + "@gymModeTimerTypeHelText": {}, + "countdown": "Countdown", + "@countdown": {}, + "stopwatch": "Stoppuhr", + "@stopwatch": {}, + "gymModeDefaultCountdownTime": "Standard-Countdown-Zeit in Sekunden", + "@gymModeDefaultCountdownTime": {}, + "gymModeNotifyOnCountdownFinish": "Benachrichtigung bei Ende des Countdowns", + "@gymModeNotifyOnCountdownFinish": {}, + "duration": "Dauer", + "@duration": {}, + "durationHoursMinutes": "{hours}h {minutes}m", + "@durationHoursMinutes": { + "description": "A duration, in hours and minutes", + "type": "text", + "placeholders": { + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + } + } + }, + "volume": "Volumen", + "@volume": { + "description": "The volume of a workout or set, i.e. weight x reps" + }, + "workoutCompleted": "Training abgeschlossen", + "@workoutCompleted": {}, + "formMinMaxValues": "Bitte geben Sie einen Wert zwischen {min} und {max}", + "@formMinMaxValues": { + "description": "Error message when the user needs to enter a value between min and max", + "type": "text", + "placeholders": { + "min": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + "superset": "Superset", + "@superset": {} } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 902d4c58..8a5cfc2c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -7,7 +7,7 @@ "@weight": { "description": "The weight of a workout log or body weight entry" }, - "confirmDelete": "Êtes-vous sûre de vouloir supprimer « {toDelete} » ?", + "confirmDelete": "Êtes-vous sûr de vouloir supprimer '{toDelete}' ?", "@confirmDelete": { "description": "Confirmation text before the user deletes an object", "type": "text", @@ -199,7 +199,7 @@ "@reset": { "description": "Button text allowing the user to reset the entered values to the default" }, - "useCustomServer": "Utiliser le serveur personnalisé", + "useCustomServer": "Utiliser un serveur personnalisé", "@useCustomServer": { "description": "Toggle button allowing users to switch between the default and a custom wger server" }, @@ -364,7 +364,7 @@ } } }, - "imageFormatNotSupportedDetail": "{imageFormat} non pris en charge", + "imageFormatNotSupportedDetail": "{imageFormat} pas encore pris en charge.", "@imageFormatNotSupportedDetail": { "description": "Label shown on the image preview container when image format is not supported", "type": "text", @@ -1126,5 +1126,63 @@ "enterTextInLanguage": "Veuillez saisir le texte dans la bonne langue !", "@enterTextInLanguage": {}, "endWorkout": "Terminer l'entraînement", - "@endWorkout": {} + "@endWorkout": {}, + "impressionGood": "Bonne", + "@impressionGood": {}, + "impressionNeutral": "Neutre", + "@impressionNeutral": {}, + "impressionBad": "Mauvaise", + "@impressionBad": {}, + "gymModeShowExercises": "Afficher les pages d'aperçu des exercices", + "@gymModeShowExercises": {}, + "gymModeShowTimer": "Afficher le chronomètre entre les séries", + "@gymModeShowTimer": {}, + "gymModeTimerType": "Type de chronomètre", + "@gymModeTimerType": {}, + "gymModeTimerTypeHelText": "Si une série a un temps de pause, un compte à rebours est toujours utilisé.", + "@gymModeTimerTypeHelText": {}, + "countdown": "Compte à rebours", + "@countdown": {}, + "stopwatch": "Chronomètre", + "@stopwatch": {}, + "gymModeDefaultCountdownTime": "Temps de compte à rebours par défaut, en secondes", + "@gymModeDefaultCountdownTime": {}, + "gymModeNotifyOnCountdownFinish": "Notifier à la fin du compte à rebours", + "@gymModeNotifyOnCountdownFinish": {}, + "duration": "Durée", + "@duration": {}, + "durationHoursMinutes": "{hours}h {minutes}m", + "@durationHoursMinutes": { + "description": "A duration, in hours and minutes", + "type": "text", + "placeholders": { + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + } + } + }, + "volume": "Volume", + "@volume": { + "description": "The volume of a workout or set, i.e. weight x reps" + }, + "workoutCompleted": "Entraînement terminé", + "@workoutCompleted": {}, + "formMinMaxValues": "Veuillez entrer une valeur entre {min} et {max}", + "@formMinMaxValues": { + "description": "Error message when the user needs to enter a value between min and max", + "type": "text", + "placeholders": { + "min": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + "superset": "Superset", + "@superset": {} } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 4e3b3012..031e49e3 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -194,5 +194,1051 @@ "slotEntryTypeNormal": "Normaal", "@slotEntryTypeNormal": {}, "slotEntryTypeDropset": "Dropset", - "@slotEntryTypeDropset": {} + "@slotEntryTypeDropset": {}, + "slotEntryTypePartial": "Deels", + "@slotEntryTypePartial": {}, + "slotEntryTypeForced": "Verplicht", + "@slotEntryTypeForced": {}, + "slotEntryTypeTut": "Tijd onder spanning", + "@slotEntryTypeTut": {}, + "slotEntryTypeIso": "Isometrische houding", + "@slotEntryTypeIso": {}, + "slotEntryTypeJump": "Springen", + "@slotEntryTypeJump": {}, + "routines": "Routines", + "@routines": {}, + "newRoutine": "Nieuwe routine", + "@newRoutine": {}, + "noRoutines": "U heeft geen routines", + "@noRoutines": {}, + "restTime": "Rust tijd", + "@restTime": {}, + "sets": "Sets", + "@sets": { + "description": "The number of sets to be done for one exercise" + }, + "rir": "RiR", + "@rir": { + "description": "Shorthand for Repetitions In Reserve" + }, + "rirNotUsed": "Ongebruikt RiR", + "@rirNotUsed": { + "description": "Label used in RiR slider when the RiR value is not used/saved for the current setting or log" + }, + "useMetric": "Gebruik metrische eenheden voor lichaamsgewicht", + "@useMetric": {}, + "weightUnit": "Gewichtseenheid", + "@weightUnit": {}, + "repetitionUnit": "Herhalingseenheid", + "@repetitionUnit": {}, + "set": "Set", + "@set": { + "description": "A set in a workout plan" + }, + "dayDescriptionHelp": "Een beschrijving van wat er deze dag is gedaan (b.v. 'pull dag') of welk lichaamsdeel getraind is (b.v. 'borst en schouders')", + "@dayDescriptionHelp": {}, + "exerciseNr": "Oefening {nr}", + "@exerciseNr": { + "description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Set Nr. xy'.", + "type": "text", + "placeholders": { + "nr": { + "type": "String" + } + } + }, + "supersetNr": "Superset {nr}", + "@supersetNr": { + "description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Superset Nr. xy'.", + "type": "text", + "placeholders": { + "nr": { + "type": "String" + } + } + }, + "sameRepetitions": "Als je dezelfde herhalingen en gewichten gebruikt voor alle sets kan je één rij invullen. Voor 4 sets kan je bijvoorbeeld 10 invullen voor de herhalingen, dit wordt dan automatisch \"4 x 10\".", + "@sameRepetitions": {}, + "impressionGood": "Goed", + "@impressionGood": {}, + "impressionNeutral": "Neutraal", + "@impressionNeutral": {}, + "impressionBad": "Slecht", + "@impressionBad": {}, + "impression": "Impressie", + "@impression": { + "description": "General impression (e.g. for a workout session) such as good, bad, etc." + }, + "notes": "Notities", + "@notes": { + "description": "Personal notes, e.g. for a workout session" + }, + "workoutSession": "Workout sessie", + "@workoutSession": { + "description": "A (logged) workout session" + }, + "restDay": "Rust dag", + "@restDay": {}, + "isRestDay": "Is rust dag", + "@isRestDay": {}, + "isRestDayHelp": "Houd er rekening mee dat alle sets en oefeningen verwijderd worden als u deze dag markeert als rust dag.", + "@isRestDayHelp": {}, + "needsLogsToAdvance": "Vereist logs om door te gaan", + "@needsLogsToAdvance": {}, + "needsLogsToAdvanceHelp": "Selecteer als je wilt dat de routine alleen doorgaat naar de volgende dag als je voor de dag een workout hebt vastgelegd", + "@needsLogsToAdvanceHelp": {}, + "routineDays": "Dagen in routine", + "@routineDays": {}, + "resultingRoutine": "Resulterende routine", + "@resultingRoutine": {}, + "newDay": "Nieuwe dag", + "@newDay": {}, + "newSet": "Nieuwe set", + "@newSet": { + "description": "Header when adding a new set to a workout day" + }, + "selectExercises": "Als je een superset wilt doen kan je naar verschillende oefeningen zoeken, en kan je ze samen groeperen", + "@selectExercises": {}, + "gymMode": "Gym modus", + "@gymMode": { + "description": "Label when starting the gym mode" + }, + "gymModeShowExercises": "Toon oefeningsoverzicht paginas", + "@gymModeShowExercises": {}, + "gymModeShowTimer": "Toon timer tussen sets", + "@gymModeShowTimer": {}, + "gymModeTimerType": "Timer type", + "@gymModeTimerType": {}, + "gymModeTimerTypeHelText": "Als een set pauze tijd heeft, wordt altijd een afteller gebruikt.", + "@gymModeTimerTypeHelText": {}, + "countdown": "Afteller", + "@countdown": {}, + "stopwatch": "Stopwatch", + "@stopwatch": {}, + "gymModeDefaultCountdownTime": "Standaard afteltijd, in secondes", + "@gymModeDefaultCountdownTime": {}, + "gymModeNotifyOnCountdownFinish": "Notificeer op einde aftelling", + "@gymModeNotifyOnCountdownFinish": {}, + "duration": "Duur", + "@duration": {}, + "durationHoursMinutes": "{hours}u {minutes}m", + "@durationHoursMinutes": { + "description": "A duration, in hours and minutes", + "type": "text", + "placeholders": { + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + } + } + }, + "volume": "Volume", + "@volume": { + "description": "The volume of a workout or set, i.e. weight x reps" + }, + "workoutCompleted": "Workout voltooid", + "@workoutCompleted": {}, + "plateCalculator": "Platen", + "@plateCalculator": { + "description": "Label used for the plate calculator in the gym mode" + }, + "plateCalculatorNotDivisible": "Niet mogelijk om gewicht te bereiken met beschikbare platen", + "@plateCalculatorNotDivisible": { + "description": "Error message when the current weight is not reachable with plates (e.g. 33.1 kg)" + }, + "pause": "Pauzeer", + "@pause": { + "description": "Noun, not an imperative! Label used for the pause when using the gym mode" + }, + "jumpTo": "Ga naar", + "@jumpTo": { + "description": "Imperative. Label used in popup allowing the user to jump to a specific exercise while in the gym mode" + }, + "todaysWorkout": "Uw workout vandaag", + "@todaysWorkout": {}, + "logHelpEntries": "Als er op een enkele dag meerdere oefeningen met hetzelfde aantal herhalingen, maar verschillende gewichten zijn, wordt in het diagram alleen de oefening met het hoogste gewicht weergegeven.", + "@logHelpEntries": {}, + "logHelpEntriesUnits": "Let op: alleen gegevens met een gewichtseenheid (kg of lb) en herhalingen worden weergegeven; andere combinaties, zoals tijd of tot uitputting, worden hier buiten beschouwing gelaten.", + "@logHelpEntriesUnits": {}, + "description": "Beschrijving", + "@description": {}, + "name": "Naam", + "@name": { + "description": "Name for a workout or nutritional plan" + }, + "save": "Opslaan", + "@save": {}, + "verify": "Bevestig", + "@verify": {}, + "addSet": "Set toevoegen", + "@addSet": { + "description": "Label for the button that adds a set (to a workout day)" + }, + "addMeal": "Maaltijd toevoegen", + "@addMeal": {}, + "mealLogged": "Maaltijd geregistreerd in dagboek", + "@mealLogged": {}, + "ingredientLogged": "Ingrediënten geregistreerd in dagboek", + "@ingredientLogged": {}, + "logMeal": "Noteer de maaltijd in je voedingsdagboek", + "@logMeal": {}, + "addIngredient": "Voeg ingrediënt toe", + "@addIngredient": {}, + "logIngredient": "Noteer ingrediënt in je voedingsdagboek", + "@logIngredient": {}, + "searchIngredient": "Zoek ingrediënt", + "@searchIngredient": { + "description": "Label on ingredient search form" + }, + "nutritionalPlan": "Voedingsplan", + "@nutritionalPlan": {}, + "nutritionalDiary": "Voedingsdagboek", + "@nutritionalDiary": {}, + "nutritionalPlans": "Voedingsplannen", + "@nutritionalPlans": {}, + "noNutritionalPlans": "U heeft geen voedingsplannen", + "@noNutritionalPlans": { + "description": "Message shown when the user has no nutritional plans" + }, + "onlyLogging": "Alleen calorieën bijhouden", + "@onlyLogging": {}, + "onlyLoggingHelpText": "Vink dit vakje aan als u alleen uw calorieën wilt registreren en geen gedetailleerd voedingsplan met specifieke maaltijden wilt opstellen", + "@onlyLoggingHelpText": {}, + "goalMacro": "Macro doelen", + "@goalMacro": { + "description": "The goal for macronutrients" + }, + "selectMealToLog": "Selecteer een maaltijd om in je dagboek te registreren", + "@selectMealToLog": {}, + "yourCurrentNutritionPlanHasNoMealsDefinedYet": "In je huidige voedingsplan zijn geen maaltijden vastgelegd", + "@yourCurrentNutritionPlanHasNoMealsDefinedYet": { + "description": "Message shown when a nutrition plan doesn't have any meals" + }, + "toAddMealsToThePlanGoToNutritionalPlanDetails": "Om maaltijden aan het plan toe te voegen, ga naar de details van het voedingsplan", + "@toAddMealsToThePlanGoToNutritionalPlanDetails": { + "description": "Message shown to guide users to the nutritional plan details page to add meals" + }, + "goalEnergy": "Energie doel", + "@goalEnergy": {}, + "goalProtein": "Proteïne doel", + "@goalProtein": {}, + "goalCarbohydrates": "Koolhydratendoel", + "@goalCarbohydrates": {}, + "goalFat": "Vet doel", + "@goalFat": {}, + "goalFiber": "Vezel doel", + "@goalFiber": {}, + "anErrorOccurred": "Er is een fout opgetreden!", + "@anErrorOccurred": {}, + "errorInfoDescription": "Het spijt ons, maar er is iets misgegaan. Je kunt ons helpen dit op te lossen door het probleem te melden op GitHub.", + "@errorInfoDescription": {}, + "errorInfoDescription2": "Je kunt de app blijven gebruiken, maar sommige functies werken mogelijk niet.", + "@errorInfoDescription2": {}, + "errorViewDetails": "Technische details", + "@errorViewDetails": {}, + "applicationLogs": "Applicatie logboek", + "@applicationLogs": {}, + "errorCouldNotConnectToServer": "Kon niet verbinden met de server", + "@errorCouldNotConnectToServer": {}, + "errorCouldNotConnectToServerDetails": "De applicatie kon geen verbinding maken met de server. Controleer uw internetverbinding of de server-URL en probeer het opnieuw. Als het probleem aanhoudt, neem dan contact op met de serverbeheerder.", + "@errorCouldNotConnectToServerDetails": {}, + "copyToClipboard": "Kopieer naar klembord", + "@copyToClipboard": {}, + "weight": "Gewicht", + "@weight": { + "description": "The weight of a workout log or body weight entry" + }, + "min": "Min", + "@min": {}, + "max": "Max", + "@max": {}, + "chartAllTimeTitle": "{name} over gehele tijd", + "@chartAllTimeTitle": { + "description": "All-time chart of 'name' (e.g. 'weight', 'body fat' etc.)", + "type": "text", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "chart30DaysTitle": "{name} afgelopen 30 dagen", + "@chart30DaysTitle": { + "description": "last 30 days chart of 'name' (e.g. 'weight', 'body fat' etc.)", + "type": "text", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "chartDuringPlanTitle": "{chartName} tijdens voedingsplan {planName}", + "@chartDuringPlanTitle": { + "description": "chart of 'chartName' (e.g. 'weight', 'body fat' etc.) logged during plan", + "type": "text", + "placeholders": { + "chartName": { + "type": "String" + }, + "planName": { + "type": "String" + } + } + }, + "measurement": "Meting", + "@measurement": {}, + "measurements": "Metingen", + "@measurements": { + "description": "Categories for the measurements such as biceps size, body fat, etc." + }, + "measurementCategoriesHelpText": "Meetcategorie, zoals 'biceps' of 'lichaamsvet'", + "@measurementCategoriesHelpText": {}, + "measurementEntriesHelpText": "De eenheid die wordt gebruikt om de categorie te meten, zoals 'cm' of '%'", + "@measurementEntriesHelpText": {}, + "date": "Datum", + "@date": { + "description": "The date of a workout log or body weight entry" + }, + "endDate": "Einddatum", + "@endDate": { + "description": "The End date of a nutritional plan or routine" + }, + "openEnded": "Open einde", + "@openEnded": { + "description": "When a nutrition plan has no pre-defined end date" + }, + "value": "Waarde", + "@value": { + "description": "The value of a measurement entry" + }, + "start": "Start", + "@start": { + "description": "Label on button to start the gym mode (i.e., an imperative)" + }, + "time": "Tijd", + "@time": { + "description": "The time of a meal or workout" + }, + "timeStart": "Start tijd", + "@timeStart": { + "description": "The starting time of a workout" + }, + "timeEnd": "Eind tijd", + "@timeEnd": { + "description": "The end time of a workout" + }, + "timeStartAhead": "De begintijd mag niet voor de eindtijd liggen", + "@timeStartAhead": {}, + "ingredient": "Ingrediënt", + "@ingredient": {}, + "energy": "Energie", + "@energy": { + "description": "Energy in a meal, ingredient etc. e.g. in kJ" + }, + "energyShort": "E", + "@energyShort": { + "description": "The first letter or short name of the word 'Energy', used in overviews" + }, + "macronutrients": "Macronutriënten", + "@macronutrients": {}, + "planned": "Gepland", + "@planned": { + "description": "Header for the column of 'planned' nutritional values, i.e. what should be eaten" + }, + "logged": "Gelogd", + "@logged": { + "description": "Header for the column of 'logged' nutritional values, i.e. what was eaten" + }, + "today": "Vandaag", + "@today": {}, + "loggedToday": "Vandaag gelogd", + "@loggedToday": {}, + "weekAverage": "7 dagen gemiddelde", + "@weekAverage": { + "description": "Header for the column of '7 day average' nutritional values, i.e. what was logged last week" + }, + "surplus": "overschot", + "@surplus": { + "description": "Caloric surplus (either planned or unplanned)" + }, + "deficit": "tekort", + "@deficit": { + "description": "Caloric deficit (either planned or unplanned)" + }, + "difference": "Verschil", + "@difference": {}, + "percentEnergy": "Procent energie", + "@percentEnergy": {}, + "gPerBodyKg": "g per lichaams kg", + "@gPerBodyKg": { + "description": "Label used for total sums of e.g. calories or similar in grams per Kg of body weight" + }, + "total": "Totaal", + "@total": { + "description": "Label used for total sums of e.g. calories or similar" + }, + "kcal": "kcal", + "@kcal": { + "description": "Energy in a meal in kilocalories, kcal" + }, + "kcalValue": "{value} kcal", + "@kcalValue": { + "description": "A value in kcal, e.g. 500 kcal", + "type": "text", + "placeholders": { + "value": { + "type": "String" + } + } + }, + "kJ": "kJ", + "@kJ": { + "description": "Energy in a meal in kilo joules, kJ" + }, + "g": "g", + "@g": { + "description": "Abbreviation for gram" + }, + "gValue": "{value} g", + "@gValue": { + "description": "A value in grams, e.g. 5 g", + "type": "text", + "placeholders": { + "value": { + "type": "String" + } + } + }, + "percentValue": "{value} %", + "@percentValue": { + "description": "A value in percent, e.g. 10 %", + "type": "text", + "placeholders": { + "value": { + "type": "String" + } + } + }, + "protein": "Proteïne", + "@protein": {}, + "proteinShort": "P", + "@proteinShort": { + "description": "The first letter or short name of the word 'Protein', used in overviews" + }, + "carbohydrates": "Koolhydraten", + "@carbohydrates": {}, + "carbohydratesShort": "K", + "@carbohydratesShort": { + "description": "The first letter or short name of the word 'Carbohydrates', used in overviews" + }, + "sugars": "Suikers", + "@sugars": {}, + "fat": "Vet", + "@fat": {}, + "fatShort": "V", + "@fatShort": { + "description": "The first letter or short name of the word 'Fat', used in overviews" + }, + "saturatedFat": "Verzadigd vet", + "@saturatedFat": {}, + "fiber": "Vezels", + "@fiber": {}, + "sodium": "Natrium", + "@sodium": {}, + "amount": "Hoeveelheid", + "@amount": { + "description": "The amount (e.g. in grams) of an ingredient in a meal" + }, + "unit": "Eenheid", + "@unit": { + "description": "The unit used for a repetition (kg, time, etc.)" + }, + "newEntry": "Nieuwe invoer", + "@newEntry": { + "description": "Title when adding a new entry such as a weight or log entry" + }, + "noWeightEntries": "Je hebt geen gewichtsinvoer", + "@noWeightEntries": { + "description": "Message shown when the user has no logged weight entries" + }, + "noMeasurementEntries": "U heeft geen meetgegevens ingevoerd", + "@noMeasurementEntries": {}, + "moreMeasurementEntries": "Nieuwe meting toevoegen", + "@moreMeasurementEntries": { + "description": "Message shown when the user wants to add new measurement" + }, + "edit": "Wijzig", + "@edit": {}, + "loadingText": "Laden...", + "@loadingText": { + "description": "Text to show when entries are being loaded in the background: Loading..." + }, + "delete": "Verwijder", + "@delete": {}, + "confirmDelete": "Weet je zeker dat je '{toDelete}' wilt verwijderen?", + "@confirmDelete": { + "description": "Confirmation text before the user deletes an object", + "type": "text", + "placeholders": { + "toDelete": { + "type": "String" + } + } + }, + "newNutritionalPlan": "Nieuw voedingsplan", + "@newNutritionalPlan": {}, + "overview": "Overzicht", + "@overview": {}, + "toggleDetails": "Schakel details in", + "@toggleDetails": { + "description": "Switch to toggle detail / overview" + }, + "goToDetailPage": "Ga naar detail pagina", + "@goToDetailPage": {}, + "aboutWhySupportTitle": "Open Source & gratis te gebruiken ❤️", + "@aboutWhySupportTitle": {}, + "aboutDescription": "Bedankt voor het gebruiken van wger! wger is een collaboratief open source project, gemaakt door fitness fans van over de hele wereld.", + "@aboutDescription": { + "description": "Text in the about dialog" + }, + "aboutDonateTitle": "Maak een donatie", + "@aboutDonateTitle": {}, + "aboutDonateText": "Hoewel het project gratis is en dat ook altijd zal blijven, zijn de kosten voor het draaien van de server dat niet! De ontwikkeling vergt bovendien veel tijd en inzet van vrijwilligers. Uw bijdrage dekt deze kosten direct en helpt de betrouwbaarheid van de dienst te waarborgen.", + "@aboutDonateText": {}, + "aboutContributeTitle": "Bijdragen", + "@aboutContributeTitle": {}, + "aboutContributeText": "Alle soorten bijdragen zijn welkom. Of je nu ontwikkelaar bent, vertaler of gewoon een fitnessliefhebber, elke vorm van steun wordt gewaardeerd!", + "@aboutContributeText": {}, + "aboutBugsListTitle": "Meld een probleem of stel een functie voor", + "@aboutBugsListTitle": {}, + "aboutTranslationListTitle": "Vertaal de applicatie", + "@aboutTranslationListTitle": {}, + "aboutSourceListTitle": "Bekijk broncode", + "@aboutSourceListTitle": {}, + "aboutJoinCommunityTitle": "Sluit je aan bij de community", + "@aboutJoinCommunityTitle": {}, + "aboutMastodonTitle": "Mastodon", + "@aboutMastodonTitle": {}, + "aboutDiscordTitle": "Discord", + "@aboutDiscordTitle": {}, + "others": "Anderen", + "@others": {}, + "calendar": "Kalender", + "@calendar": {}, + "goToToday": "Ga naar vandaag", + "@goToToday": { + "description": "Label on button to jump back to 'today' in the calendar widget" + }, + "enterValue": "Voer een waarde in", + "@enterValue": { + "description": "Error message when the user hasn't entered a value on a required field" + }, + "selectEntry": "Selecteer een entry", + "@selectEntry": {}, + "selectExercise": "Selecteer een oefening", + "@selectExercise": { + "description": "Error message when the user hasn't selected an exercise in the form" + }, + "enterCharacters": "Voer tussen {min} en {max} tekens in", + "@enterCharacters": { + "description": "Error message when the user hasn't entered the correct number of characters in a form", + "type": "text", + "placeholders": { + "min": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "formMinMaxValues": "Voer een waarde tussen {min} en {max} in", + "@formMinMaxValues": { + "description": "Error message when the user needs to enter a value between min and max", + "type": "text", + "placeholders": { + "min": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + "enterMinCharacters": "Voer minstens {min} tekens in", + "@enterMinCharacters": { + "description": "Error message when the user hasn't entered the minimum amount characters in a form", + "type": "text", + "placeholders": { + "min": { + "type": "String" + } + } + }, + "baseNameEnglish": "Alle oefeningen moeten een basisnaam in het Engels hebben", + "@baseNameEnglish": {}, + "nrOfSets": "Sets per oefening: {nrOfSets}", + "@nrOfSets": { + "description": "Label shown on the slider where the user selects the nr of sets", + "type": "text", + "placeholders": { + "nrOfSets": { + "type": "String" + } + } + }, + "setUnitsAndRir": "Stel eenheden en RiR in", + "@setUnitsAndRir": { + "description": "Label shown on the slider where the user can toggle showing units and RiR", + "type": "text" + }, + "enterValidNumber": "Voer een geldig nummer in", + "@enterValidNumber": { + "description": "Error message when the user has submitted an invalid number (e.g. '3,.,.,.')" + }, + "selectIngredient": "Selecteer een ingrediënt", + "@selectIngredient": { + "description": "Error message when the user hasn't selected an ingredient from the autocompleter" + }, + "recentlyUsedIngredients": "Recent toegevoegde ingrediënten", + "@recentlyUsedIngredients": { + "description": "A message when a user adds a new ingredient to a meal." + }, + "selectImage": "Selecteer een afbeelding", + "@selectImage": { + "description": "Label and error message when the user hasn't selected an image to save" + }, + "optionsLabel": "Opties", + "@optionsLabel": { + "description": "Label for the popup with general app options" + }, + "takePicture": "Neem een foto", + "@takePicture": {}, + "chooseFromLibrary": "Kies uit foto galerij", + "@chooseFromLibrary": {}, + "gallery": "Galerij", + "@gallery": {}, + "addImage": "Afbeelding toevoegen", + "@addImage": {}, + "dataCopied": "Gegevens gekopieerd naar een nieuw item", + "@dataCopied": { + "description": "Snackbar message to show on copying data to a new log entry" + }, + "appUpdateTitle": "Update vereist", + "@appUpdateTitle": {}, + "appUpdateContent": "Deze versie van de app is niet compatibel met de server, update uw applicatie.", + "@appUpdateContent": {}, + "productFound": "Product gevonden", + "@productFound": { + "description": "Header label for dialog when product is found with barcode" + }, + "productFoundDescription": "De barcode hoort bij dit product: {productName}. Wilt u doorgaan?", + "@productFoundDescription": { + "description": "Dialog info when product is found with barcode", + "type": "text", + "placeholders": { + "productName": { + "type": "String" + } + } + }, + "productNotFound": "Product niet gevonden", + "@productNotFound": { + "description": "Header label for dialog when product is not found with barcode" + }, + "productNotFoundDescription": "Het product met de gescande barcode {barcode} is niet gevonden in de wger database", + "@productNotFoundDescription": { + "description": "Dialog info when product is not found with barcode", + "type": "text", + "placeholders": { + "barcode": { + "type": "String" + } + } + }, + "scanBarcode": "Scan barcode", + "@scanBarcode": { + "description": "Label for scan barcode button" + }, + "close": "Sluiten", + "@close": { + "description": "Translation for close" + }, + "identicalExercisePleaseDiscard": "Als je een oefening ziet die identiek is aan degene die je toevoegt, gooi dan je concept weg en bewerk die oefening in plaats daarvan.", + "@identicalExercisePleaseDiscard": {}, + "checkInformationBeforeSubmitting": "Controleer of de ingevoerde gegevens correct zijn voordat u de oefening indient", + "@checkInformationBeforeSubmitting": {}, + "add_exercise_image_license": "Afbeeldingen moeten compatibel zijn met de CC BY SA-licentie. Upload bij twijfel alleen foto's die je zelf hebt gemaakt.", + "@add_exercise_image_license": {}, + "imageDetailsTitle": "Afbeeldingsdetails", + "@imageDetailsTitle": { + "description": "Title for image details form" + }, + "imageDetailsLicenseTitle": "Titel", + "@imageDetailsLicenseTitle": { + "description": "Label for image title field" + }, + "imageDetailsLicenseTitleHint": "Voer afbeeldingsnaam in", + "@imageDetailsLicenseTitleHint": { + "description": "Hint text for image title field" + }, + "imageDetailsSourceLink": "Link naar de bronwebsite", + "@imageDetailsSourceLink": { + "description": "Label for source link field" + }, + "author": "Auteur(s)", + "@author": {}, + "authorHint": "Voer auteursnaam in", + "@authorHint": { + "description": "Hint text for author field" + }, + "imageDetailsAuthorLink": "Link naar de website of het profiel van de auteur", + "@imageDetailsAuthorLink": { + "description": "Label for author link field" + }, + "imageDetailsDerivativeSource": "Link naar de originele bron, als dit een afgeleid werk is", + "@imageDetailsDerivativeSource": { + "description": "Label for derivative source field" + }, + "imageDetailsDerivativeHelp": "Hulptekst ter uitleg van afgeleide werken.", + "@imageDetailsDerivativeHelp": { + "description": "Helper text explaining derivative works" + }, + "imageDetailsImageType": "Afbeeldingstype", + "@imageDetailsImageType": { + "description": "Label for image type selector" + }, + "imageDetailsLicenseNotice": "Door deze afbeelding in te dienen, stemt u ermee in deze vrij te geven onder de CC-BY-SA-4-licentie. De afbeelding moet uw eigen werk zijn of de auteur moet deze hebben vrijgegeven onder een licentie die hiermee compatibel is.", + "@imageDetailsLicenseNotice": {}, + "imageDetailsLicenseNoticeLinkToLicense": "Zie licentie tekst.", + "@imageDetailsLicenseNoticeLinkToLicense": {}, + "imageFormatNotSupported": "{imageFormat} wordt niet ondersteund", + "@imageFormatNotSupported": { + "description": "Label shown on the error container when image format is not supported", + "type": "text", + "placeholders": { + "imageFormat": { + "type": "String" + } + } + }, + "imageFormatNotSupportedDetail": "Afbeeldingen met het formaat {imageFormat} worden nog niet ondersteund.", + "@imageFormatNotSupportedDetail": { + "description": "Label shown on the image preview container when image format is not supported", + "type": "text", + "placeholders": { + "imageFormat": { + "type": "String" + } + } + }, + "add": "toevoegen", + "@add": { + "description": "Add button text" + }, + "variations": "Variaties", + "@variations": { + "description": "Variations of one exercise (e.g. benchpress and benchpress narrow)" + }, + "alsoKnownAs": "Ook bekend als: {aliases}", + "@alsoKnownAs": { + "placeholders": { + "aliases": { + "type": "String" + } + }, + "description": "List of alternative names for an exercise" + }, + "verifiedEmail": "Geverifieerde email", + "@verifiedEmail": {}, + "unVerifiedEmail": "Niet-geverifieerde e-mail", + "@unVerifiedEmail": {}, + "verifiedEmailReason": "Je moet je e-mailadres verifiëren om oefeningen te kunnen doen", + "@verifiedEmailReason": {}, + "verifiedEmailInfo": "Er is een verificatiemail verzonden naar {email}", + "@verifiedEmailInfo": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "alternativeNames": "Alternatieve namen", + "@alternativeNames": {}, + "oneNamePerLine": "Een naam per lijn", + "@oneNamePerLine": {}, + "whatVariationsExist": "Welke varianten van deze oefening bestaan er, indien van toepassing?", + "@whatVariationsExist": {}, + "previous": "Vorige", + "@previous": {}, + "next": "Volgende", + "@next": {}, + "images": "Afbeeldingen", + "@images": {}, + "language": "Taal", + "@language": {}, + "addExercise": "Voeg oefening toe", + "@addExercise": {}, + "fitInWeek": "In week passen", + "@fitInWeek": {}, + "fitInWeekHelp": "Indien ingeschakeld, zullen de dagen zich herhalen in een wekelijkse cyclus; anders zullen de dagen elkaar opeenvolgend opvolgen, ongeacht het begin van een nieuwe week.", + "@fitInWeekHelp": {}, + "addSuperset": "Superset toevoegen", + "@addSuperset": {}, + "superset": "Superset", + "@superset": {}, + "setHasProgression": "Set heeft progressie", + "@setHasProgression": {}, + "setHasProgressionWarning": "Houd er rekening mee dat het momenteel niet mogelijk is om alle instellingen voor een set te bewerken in de mobiele app of om de automatische voortgang te configureren. Gebruik hiervoor voorlopig de webapp.", + "@setHasProgressionWarning": {}, + "setHasNoExercises": "Deze set heeft nog geen oefeningen!", + "@setHasNoExercises": {}, + "contributeExercise": "Draag een oefening bij", + "@contributeExercise": {}, + "translation": "Vertaling", + "@translation": {}, + "translateExercise": "Vertaal deze oefening", + "@translateExercise": {}, + "baseData": "Basics in Engels", + "@baseData": { + "description": "The base data for an exercise such as category, trained muscles, etc." + }, + "enterTextInLanguage": "Voer de tekst in de juiste taal in!", + "@enterTextInLanguage": {}, + "settingsTitle": "Instellingen", + "@settingsTitle": {}, + "settingsCacheTitle": "Cache", + "@settingsCacheTitle": {}, + "settingsExerciseCacheDescription": "Oefeningscache", + "@settingsExerciseCacheDescription": {}, + "settingsIngredientCacheDescription": "Ingrediënten cache", + "@settingsIngredientCacheDescription": {}, + "settingsCacheDeletedSnackbar": "Cache succesvol geleegd", + "@settingsCacheDeletedSnackbar": {}, + "aboutPageTitle": "Over ons & Support", + "@aboutPageTitle": {}, + "contributeExerciseWarning": "Je kunt alleen oefeningen bijdragen als je account ouder is dan {days} dagen en je e-mailadres is geverifieerd", + "@contributeExerciseWarning": { + "description": "Number of days before which a person can add exercise", + "placeholders": { + "days": { + "type": "String", + "example": "14" + } + } + }, + "simpleMode": "Eenvoudige modus", + "@simpleMode": {}, + "simpleModeHelp": "Verberg enkele van de meer geavanceerde instellingen tijdens het bewerken van oefeningen", + "@simpleModeHelp": {}, + "progressionRules": "Deze oefening heeft voortgangsregels en kan niet worden bewerkt in de mobiele app. Gebruik de webapplicatie om deze oefening te bewerken.", + "@progressionRules": {}, + "cacheWarning": "Vanwege de caching kan het even duren voordat de wijzigingen in de hele applicatie zichtbaar zijn.", + "@cacheWarning": {}, + "textPromptTitle": "Klaar om te starten?", + "@textPromptTitle": {}, + "textPromptSubheading": "Druk op de actieknop om te beginnen", + "@textPromptSubheading": {}, + "abs": "Buikspieren", + "@abs": { + "description": "Generated entry for translation for server strings" + }, + "arms": "Armen", + "@arms": { + "description": "Generated entry for translation for server strings" + }, + "back": "Rug", + "@back": { + "description": "Generated entry for translation for server strings" + }, + "barbell": "Barbell", + "@barbell": { + "description": "Generated entry for translation for server strings" + }, + "bench": "Bench", + "@bench": { + "description": "Generated entry for translation for server strings" + }, + "biceps": "Biceps", + "@biceps": { + "description": "Generated entry for translation for server strings" + }, + "body_weight": "Gewicht", + "@body_weight": { + "description": "Generated entry for translation for server strings" + }, + "calves": "Kuiten", + "@calves": { + "description": "Generated entry for translation for server strings" + }, + "cardio": "Cardio", + "@cardio": { + "description": "Generated entry for translation for server strings" + }, + "chest": "Borst", + "@chest": { + "description": "Generated entry for translation for server strings" + }, + "dumbbell": "Dumbbell", + "@dumbbell": { + "description": "Generated entry for translation for server strings" + }, + "glutes": "Glutes", + "@glutes": { + "description": "Generated entry for translation for server strings" + }, + "gym_mat": "Gym matje", + "@gym_mat": { + "description": "Generated entry for translation for server strings" + }, + "hamstrings": "Hamstrings", + "@hamstrings": { + "description": "Generated entry for translation for server strings" + }, + "incline_bench": "Incline bench", + "@incline_bench": { + "description": "Generated entry for translation for server strings" + }, + "kettlebell": "Kettlebell", + "@kettlebell": { + "description": "Generated entry for translation for server strings" + }, + "kilometers": "Kilometers", + "@kilometers": { + "description": "Generated entry for translation for server strings" + }, + "kilometers_per_hour": "Kilometers Per Uur", + "@kilometers_per_hour": { + "description": "Generated entry for translation for server strings" + }, + "lats": "Lats", + "@lats": { + "description": "Generated entry for translation for server strings" + }, + "legs": "Benen", + "@legs": { + "description": "Generated entry for translation for server strings" + }, + "lower_back": "Onderrug", + "@lower_back": { + "description": "Generated entry for translation for server strings" + }, + "max_reps": "Max Herhalingen", + "@max_reps": { + "description": "Generated entry for translation for server strings" + }, + "miles": "Miles", + "@miles": { + "description": "Generated entry for translation for server strings" + }, + "miles_per_hour": "Miles Per Uur", + "@miles_per_hour": { + "description": "Generated entry for translation for server strings" + }, + "minutes": "Minuten", + "@minutes": { + "description": "Generated entry for translation for server strings" + }, + "plates": "Platen", + "@plates": { + "description": "Generated entry for translation for server strings" + }, + "pull_up_bar": "Pull-up bar", + "@pull_up_bar": { + "description": "Generated entry for translation for server strings" + }, + "quads": "Quads", + "@quads": { + "description": "Generated entry for translation for server strings" + }, + "repetitions": "Herhalingen", + "@repetitions": { + "description": "Generated entry for translation for server strings" + }, + "resistance_band": "Weerstandsband", + "@resistance_band": { + "description": "Generated entry for translation for server strings" + }, + "sz_bar": "SZ-Bar", + "@sz_bar": { + "description": "Generated entry for translation for server strings" + }, + "seconds": "Seconden", + "@seconds": { + "description": "Generated entry for translation for server strings" + }, + "shoulders": "Schouders", + "@shoulders": { + "description": "Generated entry for translation for server strings" + }, + "swiss_ball": "Zwitserse Bal", + "@swiss_ball": { + "description": "Generated entry for translation for server strings" + }, + "triceps": "Triceps", + "@triceps": { + "description": "Generated entry for translation for server strings" + }, + "until_failure": "Tot Falen", + "@until_failure": { + "description": "Generated entry for translation for server strings" + }, + "kg": "kg", + "@kg": { + "description": "Generated entry for translation for server strings" + }, + "lb": "lb", + "@lb": { + "description": "Generated entry for translation for server strings" + }, + "none__bodyweight_exercise_": "geen (lichaamsgewicht)", + "@none__bodyweight_exercise_": { + "description": "Generated entry for translation for server strings" + }, + "log": "Vastleggen", + "@log": { + "description": "Log a specific meal (imperative form)" + }, + "done": "Klaar", + "@done": {}, + "overallChangeWeight": "Algemene verandering", + "@overallChangeWeight": { + "description": "Overall change in weight, added for localization" + }, + "goalTypeMeals": "Van maaltijden", + "@goalTypeMeals": { + "description": "added for localization of Class GoalType's filed meals" + }, + "goalTypeBasic": "Basic", + "@goalTypeBasic": { + "description": "added for localization of Class GoalType's filed basic" + }, + "goalTypeAdvanced": "Geavanceerd", + "@goalTypeAdvanced": { + "description": "added for localization of Class GoalType's filed advanced" + }, + "indicatorRaw": "rauw", + "@indicatorRaw": { + "description": "added for localization of Class Indicator's field text" + }, + "indicatorAvg": "gemiddeld", + "@indicatorAvg": { + "description": "added for localization of Class Indicator's field text" + }, + "endWorkout": "Beëindig workout", + "@endWorkout": { + "description": "Use the imperative, label on button to finish the current workout in gym mode" + }, + "themeMode": "Thema modus", + "@themeMode": {}, + "darkMode": "Altijd donkere modus", + "@darkMode": {}, + "lightMode": "Altijd lichte modus", + "@lightMode": {}, + "systemMode": "Systeem instellingen", + "@systemMode": {}, + "slotEntryTypeMyo": "Myo", + "@slotEntryTypeMyo": {} } diff --git a/lib/l10n/app_pt_BR.arb b/lib/l10n/app_pt_BR.arb index 85ff9ccf..2e3d2e26 100644 --- a/lib/l10n/app_pt_BR.arb +++ b/lib/l10n/app_pt_BR.arb @@ -115,7 +115,7 @@ "@close": { "description": "Translation for close" }, - "successfullyDeleted": "Excluído", + "successfullyDeleted": "Removido com êxito", "@successfullyDeleted": { "description": "Message when an item was successfully deleted" }, @@ -125,13 +125,13 @@ "@goToToday": { "description": "Label on button to jump back to 'today' in the calendar widget" }, - "set": "Definir", + "set": "Série", "@set": { "description": "A set in a workout plan" }, "noMeasurementEntries": "Você não tem entradas de medição", "@noMeasurementEntries": {}, - "newSet": "Novo conjunto", + "newSet": "Nova séries", "@newSet": { "description": "Header when adding a new set to a workout day" }, @@ -189,7 +189,7 @@ "@pause": { "description": "Noun, not an imperative! Label used for the pause when using the gym mode" }, - "success": "Sucesso", + "success": "Bem-sucedido", "@success": { "description": "Message when an action completed successfully, usually used as a heading" }, @@ -213,7 +213,7 @@ "@newEntry": { "description": "Title when adding a new entry such as a weight or log entry" }, - "addSet": "Adicionar set", + "addSet": "Adicionar séries", "@addSet": { "description": "Label for the button that adds a set (to a workout day)" }, @@ -271,7 +271,7 @@ "@loadingText": { "description": "Text to show when entries are being loaded in the background: Loading..." }, - "selectExercises": "Se quiser fazer um superset você pode procurar vários exercícios, eles estarão agrupados", + "selectExercises": "Se quiser fazer um superséries você pode procurar vários exercícios, eles estarão agrupados", "@selectExercises": {}, "nutritionalDiary": "Diário nutricional", "@nutritionalDiary": {}, @@ -953,7 +953,7 @@ "@noRoutines": {}, "restTime": "Tempo de descanso", "@restTime": {}, - "sets": "Conjuntos", + "sets": "Séries", "@sets": { "description": "The number of sets to be done for one exercise" }, @@ -967,7 +967,7 @@ } } }, - "supersetNr": "Superset {nr}", + "supersetNr": "Supersérie {nr}", "@supersetNr": { "description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Superset Nr. xy'.", "type": "text", @@ -981,7 +981,7 @@ "@restDay": {}, "isRestDay": "É dia de descanso", "@isRestDay": {}, - "isRestDayHelp": "Por favor, note que todos os conjuntos e exercícios serão removidos quando marcar um dia como um dia de descanso.", + "isRestDayHelp": "Por favor, note que todos as séries e exercícios serão removidos quando marcar um dia como um dia de descanso.", "@isRestDayHelp": {}, "needsLogsToAdvance": "Precisa de logs para avançar", "@needsLogsToAdvance": {}, @@ -1005,7 +1005,7 @@ "@toAddMealsToThePlanGoToNutritionalPlanDetails": { "description": "Message shown to guide users to the nutritional plan details page to add meals" }, - "errorInfoDescription": "Algo de errado aconteceu. Você pode nos ajudar a concertar esse problema reportando o problema no Github", + "errorInfoDescription": "Algo de errado aconteceu. Você pode nos ajudar a concertar esse problema reportando o problema no Github.", "@errorInfoDescription": {}, "errorInfoDescription2": "Você pode continuar usando o applicativo, mas algumas funcionalidades não estarão disponíveis.", "@errorInfoDescription2": {}, @@ -1021,7 +1021,7 @@ "@min": {}, "max": "Max", "@max": {}, - "aboutWhySupportTitle": "Open Source & free to use ❤️", + "aboutWhySupportTitle": "Código aberto e de uso gratuito ❤️", "@aboutWhySupportTitle": {}, "aboutContributeTitle": "Contribua", "@aboutContributeTitle": {}, @@ -1043,13 +1043,13 @@ "@fitInWeek": {}, "fitInWeekHelp": "Se ligado, os dias vão se repetir semanalmente, caso contrário os dias seguirão sequencialmente se considerar o começo de uma nova semana.", "@fitInWeekHelp": {}, - "addSuperset": "Adicionar superset", + "addSuperset": "Adicionar superséries", "@addSuperset": {}, "setHasProgression": "Treino tem prograssão", "@setHasProgression": {}, - "setHasProgressionWarning": "Observe que, no momento, não é possível editar todas as configurações de um conjunto no aplicativo móvel nem configurar a progressão automática. Por enquanto, use o aplicativo web.", + "setHasProgressionWarning": "Observe que, no momento, não é possível editar todas as configurações de um séries no aplicativo móvel nem configurar a progressão automática. Por enquanto, use o aplicativo web.", "@setHasProgressionWarning": {}, - "setHasNoExercises": "Este treino ainda não tem exercícios!", + "setHasNoExercises": "Este séries ainda não tem exercícios!", "@setHasNoExercises": {}, "simpleMode": "Modo simples", "@simpleMode": {}, @@ -1057,7 +1057,7 @@ "@simpleModeHelp": {}, "progressionRules": "Este exercício tem regras de progressão e não pode ser editado no aplicativo móvel. Use o aplicativo web para editá-lo.", "@progressionRules": {}, - "resistance_band": "Resistance band", + "resistance_band": "Banda de resistência", "@resistance_band": { "description": "Generated entry for translation for server strings" }, @@ -1077,8 +1077,176 @@ "@startDate": {}, "dayTypeCustom": "Personalizado", "@dayTypeCustom": {}, - "dayTypeHiit": "Treino de alta intensidade", + "dayTypeHiit": "Treinamento intervalado de alta intensidade", "@dayTypeHiit": {}, - "dayTypeTabata": "Tabata", - "@dayTypeTabata": {} + "dayTypeTabata": "Método Tabata", + "@dayTypeTabata": {}, + "impressionGood": "Boa", + "@impressionGood": {}, + "impressionNeutral": "Neutra", + "@impressionNeutral": {}, + "impressionBad": "Ruim", + "@impressionBad": {}, + "gymModeShowExercises": "Mostrar páginas de visão geral dos exercícios", + "@gymModeShowExercises": {}, + "gymModeShowTimer": "Mostrar cronômetro entre séries", + "@gymModeShowTimer": {}, + "gymModeTimerType": "Tipo de temporizador", + "@gymModeTimerType": {}, + "gymModeTimerTypeHelText": "Se uma série tiver tempo de pausa, sempre será usada uma contagem regressiva.", + "@gymModeTimerTypeHelText": {}, + "countdown": "Contagem regressiva", + "@countdown": {}, + "stopwatch": "cronômetro", + "@stopwatch": {}, + "gymModeDefaultCountdownTime": "Tempo de contagem regressiva padrão, em segundos", + "@gymModeDefaultCountdownTime": {}, + "gymModeNotifyOnCountdownFinish": "Notificar no final da contagem regressiva", + "@gymModeNotifyOnCountdownFinish": {}, + "duration": "Duração", + "@duration": {}, + "durationHoursMinutes": "{hours}h {minutes}m", + "@durationHoursMinutes": { + "description": "A duration, in hours and minutes", + "type": "text", + "placeholders": { + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + } + } + }, + "volume": "Volume", + "@volume": { + "description": "The volume of a workout or set, i.e. weight x reps" + }, + "workoutCompleted": "Treino concluído", + "@workoutCompleted": {}, + "dayTypeEnom": "Cada minuto a minuto", + "@dayTypeEnom": {}, + "dayTypeAmrap": "Tantas rodadas quanto possível", + "@dayTypeAmrap": {}, + "dayTypeEdt": "Treinamento de densidade crescente", + "@dayTypeEdt": {}, + "dayTypeRft": "Rodadas para ganhar tempo", + "@dayTypeRft": {}, + "dayTypeAfap": "O mais rápido possível", + "@dayTypeAfap": {}, + "slotEntryTypeNormal": "Normal", + "@slotEntryTypeNormal": {}, + "slotEntryTypePartial": "Parcial", + "@slotEntryTypePartial": {}, + "slotEntryTypeForced": "Forçado", + "@slotEntryTypeForced": {}, + "slotEntryTypeTut": "Tempo Sob Tensão", + "@slotEntryTypeTut": {}, + "slotEntryTypeIso": "Fixação isométrica", + "@slotEntryTypeIso": {}, + "slotEntryTypeJump": "Pular", + "@slotEntryTypeJump": {}, + "applicationLogs": "Registros de aplicativos", + "@applicationLogs": {}, + "openEnded": "Aberto", + "@openEnded": { + "description": "When a nutrition plan has no pre-defined end date" + }, + "overview": "visão global", + "@overview": {}, + "formMinMaxValues": "Insira um valor entre {min} e {max}", + "@formMinMaxValues": { + "description": "Error message when the user needs to enter a value between min and max", + "type": "text", + "placeholders": { + "min": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + "identicalExercisePleaseDiscard": "Se você notar um exercício idêntico ao que você está adicionando, descarte o rascunho e edite o exercício.", + "@identicalExercisePleaseDiscard": {}, + "checkInformationBeforeSubmitting": "Verifique se as informações inseridas estão corretas antes de enviar o exercício", + "@checkInformationBeforeSubmitting": {}, + "imageDetailsTitle": "Detalhes da imagem", + "@imageDetailsTitle": { + "description": "Title for image details form" + }, + "imageDetailsLicenseTitle": "Titulo", + "@imageDetailsLicenseTitle": { + "description": "Label for image title field" + }, + "imageDetailsLicenseTitleHint": "Insira o título da imagem", + "@imageDetailsLicenseTitleHint": { + "description": "Hint text for image title field" + }, + "imageDetailsSourceLink": "Link para o site de origem", + "@imageDetailsSourceLink": { + "description": "Label for source link field" + }, + "author": "Autor(s)", + "@author": {}, + "authorHint": "Digite o nome do autor", + "@authorHint": { + "description": "Hint text for author field" + }, + "imageDetailsAuthorLink": "Link para o site ou perfil do autor", + "@imageDetailsAuthorLink": { + "description": "Label for author link field" + }, + "imageDetailsDerivativeSource": "Link para a fonte original, se este for um trabalho derivado", + "@imageDetailsDerivativeSource": { + "description": "Label for derivative source field" + }, + "imageDetailsDerivativeHelp": "Um trabalho derivado é baseado em um trabalho anterior, mas contém conteúdo novo e criativo suficiente para ter direito aos seus próprios direitos autorais.", + "@imageDetailsDerivativeHelp": { + "description": "Helper text explaining derivative works" + }, + "imageDetailsImageType": "Tipo de imagem", + "@imageDetailsImageType": { + "description": "Label for image type selector" + }, + "imageDetailsLicenseNotice": "Ao enviar esta imagem, você concorda em liberá-la sob CC-BY-SA-4. A imagem deve ser de sua autoria ou o autor deve tê-la divulgado sob uma licença compatível com ela.", + "@imageDetailsLicenseNotice": {}, + "imageDetailsLicenseNoticeLinkToLicense": "Consulte o texto da licença.", + "@imageDetailsLicenseNoticeLinkToLicense": {}, + "imageFormatNotSupported": "{imageFormat} não compatível", + "@imageFormatNotSupported": { + "description": "Label shown on the error container when image format is not supported", + "type": "text", + "placeholders": { + "imageFormat": { + "type": "String" + } + } + }, + "imageFormatNotSupportedDetail": "Imagens {imageFormat} ainda não são suportadas.", + "@imageFormatNotSupportedDetail": { + "description": "Label shown on the image preview container when image format is not supported", + "type": "text", + "placeholders": { + "imageFormat": { + "type": "String" + } + } + }, + "add": "adicionar", + "@add": { + "description": "Add button text" + }, + "superset": "Supersérie", + "@superset": {}, + "enterTextInLanguage": "Por favor, insira o texto no idioma correto!", + "@enterTextInLanguage": {}, + "endWorkout": "Terminar treino", + "@endWorkout": { + "description": "Use the imperative, label on button to finish the current workout in gym mode" + }, + "slotEntryTypeMyo": "Myo", + "@slotEntryTypeMyo": {}, + "slotEntryTypeDropset": "Drop set", + "@slotEntryTypeDropset": {} } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 1d88c768..243d31f6 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1162,5 +1162,63 @@ "slotEntryTypeJump": "Стрибок", "@slotEntryTypeJump": {}, "endWorkout": "Закінчити тренування", - "@endWorkout": {} + "@endWorkout": {}, + "impressionGood": "Добре", + "@impressionGood": {}, + "impressionNeutral": "Нейтральний", + "@impressionNeutral": {}, + "impressionBad": "Погано", + "@impressionBad": {}, + "gymModeShowExercises": "Показати сторінки огляду вправ", + "@gymModeShowExercises": {}, + "gymModeShowTimer": "Показувати таймер між сетами", + "@gymModeShowTimer": {}, + "gymModeTimerType": "Тип таймера", + "@gymModeTimerType": {}, + "gymModeTimerTypeHelText": "Якщо сет має час паузи, завжди використовується зворотний відлік.", + "@gymModeTimerTypeHelText": {}, + "countdown": "Зворотний відлік", + "@countdown": {}, + "stopwatch": "Секундомір", + "@stopwatch": {}, + "gymModeDefaultCountdownTime": "Час зворотного відліку за замовчуванням, у секундах", + "@gymModeDefaultCountdownTime": {}, + "gymModeNotifyOnCountdownFinish": "Повідомити про закінчення зворотного відліку", + "@gymModeNotifyOnCountdownFinish": {}, + "duration": "Тривалість", + "@duration": {}, + "durationHoursMinutes": "{hours}г {minutes}хв", + "@durationHoursMinutes": { + "description": "A duration, in hours and minutes", + "type": "text", + "placeholders": { + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + } + } + }, + "volume": "Обсяг", + "@volume": { + "description": "The volume of a workout or set, i.e. weight x reps" + }, + "workoutCompleted": "Тренування завершено", + "@workoutCompleted": {}, + "formMinMaxValues": "Будь ласка, введіть значення від {min} до {max}", + "@formMinMaxValues": { + "description": "Error message when the user needs to enter a value between min and max", + "type": "text", + "placeholders": { + "min": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + "superset": "Суперсет", + "@superset": {} } diff --git a/lib/main.dart b/lib/main.dart index db8836c1..47761881 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,8 +21,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart' as riverpod; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/core/locator.dart'; -import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/models/measurements/measurement_category.dart b/lib/models/measurements/measurement_category.dart index d32925db..e5c8cdfd 100644 --- a/lib/models/measurements/measurement_category.dart +++ b/lib/models/measurements/measurement_category.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/models/measurements/measurement_entry.dart'; part 'measurement_category.g.dart'; diff --git a/lib/models/workouts/log.dart b/lib/models/workouts/log.dart index 0c192928..495ff7d3 100644 --- a/lib/models/workouts/log.dart +++ b/lib/models/workouts/log.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -51,7 +51,7 @@ class Log { late int routineId; @JsonKey(required: true, name: 'session') - late int? sessionId; + int? sessionId; @JsonKey(required: true) int? iteration; @@ -72,22 +72,22 @@ class Log { num? repetitionsTarget; @JsonKey(required: true, name: 'repetitions_unit') - late int? repetitionsUnitId; + int? repetitionsUnitId; @JsonKey(includeFromJson: false, includeToJson: false) - late RepetitionUnit? repetitionsUnitObj; + RepetitionUnit? repetitionsUnitObj; @JsonKey(required: true, fromJson: stringToNumNull, toJson: numToString) - late num? weight; + num? weight; @JsonKey(required: true, fromJson: stringToNumNull, toJson: numToString, name: 'weight_target') num? weightTarget; @JsonKey(required: true, name: 'weight_unit') - late int? weightUnitId; + int? weightUnitId; @JsonKey(includeFromJson: false, includeToJson: false) - late WeightUnit? weightUnitObj; + WeightUnit? weightUnitObj; @JsonKey(required: true, fromJson: utcIso8601ToLocalDate, toJson: dateToUtcIso8601) late DateTime date; @@ -101,41 +101,78 @@ class Log { this.repetitions, this.repetitionsTarget, this.repetitionsUnitId = REP_UNIT_REPETITIONS_ID, - required this.rir, + this.rir, this.rirTarget, this.weight, this.weightTarget, this.weightUnitId = WEIGHT_UNIT_KG, - required this.date, - }); + DateTime? date, + }) : date = date ?? DateTime.now(); Log.empty(); - Log.fromSetConfigData(SetConfigData data) { + Log.fromSetConfigData(SetConfigData setConfig) { date = DateTime.now(); sessionId = null; - slotEntryId = data.slotEntryId; - exerciseBase = data.exercise; + slotEntryId = setConfig.slotEntryId; + exerciseBase = setConfig.exercise; - if (data.weight != null) { - weight = data.weight; - weightTarget = data.weight; - } - if (data.weightUnit != null) { - weightUnit = data.weightUnit; + weight = setConfig.weight; + weightTarget = setConfig.weight; + weightUnit = setConfig.weightUnit; + + repetitions = setConfig.repetitions; + repetitionsTarget = setConfig.repetitions; + repetitionUnit = setConfig.repetitionsUnit; + + rir = setConfig.rir; + rirTarget = setConfig.rir; + } + + Log copyWith({ + int? id, + int? exerciseId, + int? routineId, + int? sessionId, + int? iteration, + int? slotEntryId, + num? rir, + num? rirTarget, + num? repetitions, + num? repetitionsTarget, + int? repetitionsUnitId, + num? weight, + num? weightTarget, + int? weightUnitId, + DateTime? date, + }) { + final out = Log( + id: id ?? this.id, + exerciseId: exerciseId ?? this.exerciseId, + iteration: iteration ?? this.iteration, + slotEntryId: slotEntryId ?? this.slotEntryId, + routineId: routineId ?? this.routineId, + repetitions: repetitions ?? this.repetitions, + repetitionsTarget: repetitionsTarget ?? this.repetitionsTarget, + repetitionsUnitId: repetitionsUnitId ?? this.repetitionsUnitId, + rir: rir ?? this.rir, + rirTarget: rirTarget ?? this.rirTarget, + weight: weight ?? this.weight, + weightTarget: weightTarget ?? this.weightTarget, + weightUnitId: weightUnitId ?? this.weightUnitId, + date: date ?? this.date, + ); + + if (sessionId != null) { + out.sessionId = sessionId; } - if (data.repetitions != null) { - repetitions = data.repetitions; - repetitionsTarget = data.repetitions; - } - if (data.repetitionsUnit != null) { - repetitionUnit = data.repetitionsUnit; - } + out.exerciseBase = exercise; + out.repetitionUnit = repetitionsUnitObj; + out.weightUnitObj = weightUnitObj; - rir = data.rir; - rirTarget = data.rir; + return out; } // Boilerplate diff --git a/lib/models/workouts/session.dart b/lib/models/workouts/session.dart index c90edaf2..a96f4db8 100644 --- a/lib/models/workouts/session.dart +++ b/lib/models/workouts/session.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,9 +16,11 @@ * along with this program. If not, see . */ +import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:logging/logging.dart'; +import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/log.dart'; @@ -62,14 +64,14 @@ class WorkoutSession { this.id, this.dayId, required this.routineId, - this.impression = 2, + this.impression = DEFAULT_IMPRESSION, this.notes = '', this.timeStart, this.timeEnd, this.logs = const [], DateTime? date, }) { - this.date = date ?? DateTime.now(); + this.date = date ?? clock.now(); } Duration? get duration { diff --git a/lib/models/workouts/session.g.dart b/lib/models/workouts/session.g.dart index ac78029d..bf061fa1 100644 --- a/lib/models/workouts/session.g.dart +++ b/lib/models/workouts/session.g.dart @@ -23,7 +23,9 @@ WorkoutSession _$WorkoutSessionFromJson(Map json) { id: (json['id'] as num?)?.toInt(), dayId: (json['day'] as num?)?.toInt(), routineId: (json['routine'] as num?)?.toInt(), - impression: json['impression'] == null ? 2 : int.parse(json['impression'] as String), + impression: json['impression'] == null + ? DEFAULT_IMPRESSION + : int.parse(json['impression'] as String), notes: json['notes'] as String? ?? '', timeStart: stringToTimeNull(json['time_start'] as String?), timeEnd: stringToTimeNull(json['time_end'] as String?), diff --git a/lib/models/workouts/slot_data.dart b/lib/models/workouts/slot_data.dart index cd36464e..9ef73f35 100644 --- a/lib/models/workouts/slot_data.dart +++ b/lib/models/workouts/slot_data.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -36,8 +36,8 @@ class SlotData { late List setConfigs; SlotData({ - required this.comment, - required this.isSuperset, + this.comment = '', + this.isSuperset = false, this.exerciseIds = const [], this.setConfigs = const [], }); diff --git a/lib/models/workouts/slot_data.g.dart b/lib/models/workouts/slot_data.g.dart index a8d70f14..756717e6 100644 --- a/lib/models/workouts/slot_data.g.dart +++ b/lib/models/workouts/slot_data.g.dart @@ -12,8 +12,8 @@ SlotData _$SlotDataFromJson(Map json) { requiredKeys: const ['comment', 'is_superset', 'exercises', 'sets'], ); return SlotData( - comment: json['comment'] as String, - isSuperset: json['is_superset'] as bool, + comment: json['comment'] as String? ?? '', + isSuperset: json['is_superset'] as bool? ?? false, exerciseIds: (json['exercises'] as List?)?.map((e) => (e as num).toInt()).toList() ?? const [], setConfigs: diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index a00a292a..3f2e26fa 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -27,7 +27,7 @@ import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:version/version.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/shared_preferences.dart'; @@ -58,7 +58,7 @@ class AuthProvider with ChangeNotifier { static const SERVER_VERSION_URL = 'version'; static const REGISTRATION_URL = 'register'; static const LOGIN_URL = 'login'; - static const TEST_URL = 'userprofile'; + static const USERPROFILE_URL = 'userprofile'; late http.Client client; @@ -132,7 +132,7 @@ class AuthProvider with ChangeNotifier { ); if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } return login(username, password, serverUrl, null); @@ -150,7 +150,7 @@ class AuthProvider with ChangeNotifier { // Login using the API token if (apiToken != null && apiToken.isNotEmpty) { final response = await client.get( - makeUri(serverUrl, TEST_URL), + makeUri(serverUrl, USERPROFILE_URL), headers: { HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', HttpHeaders.userAgentHeader: getAppNameHeader(), @@ -158,8 +158,8 @@ class AuthProvider with ChangeNotifier { }, ); - if (response.statusCode != 200) { - throw WgerHttpException(response.body); + if (response.statusCode >= 400) { + throw WgerHttpException(response); } token = apiToken; @@ -169,17 +169,18 @@ class AuthProvider with ChangeNotifier { final response = await client.post( makeUri(serverUrl, LOGIN_URL), headers: { - HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', + 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(response.body); + throw WgerHttpException(response); } + final responseData = json.decode(response.body); + token = responseData['token']; } diff --git a/lib/providers/base_provider.dart b/lib/providers/base_provider.dart index 351efe99..b905e29c 100644 --- a/lib/providers/base_provider.dart +++ b/lib/providers/base_provider.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,19 +16,27 @@ * along with this program. If not, see . */ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:math' as math; import 'package:http/http.dart' as http; import 'package:http/http.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:logging/logging.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/providers/auth.dart'; import 'package:wger/providers/helpers.dart'; +/// initial delay for fetch retries, in milliseconds +const FETCH_INITIAL_DELAY = 250; + /// Base provider class. /// /// Provides a couple of comfort functions so we avoid a bit of boilerplate. class WgerBaseProvider { + final _logger = Logger('WgerBaseProvider'); + AuthProvider auth; late http.Client client; @@ -56,21 +64,53 @@ class WgerBaseProvider { } /// Fetch and retrieve the overview list of objects, returns the JSON parsed response - Future fetch(Uri uri) async { - // Future | List> fetch(Uri uri) async { - // Send the request - final response = await client.get( - uri, - headers: getDefaultHeaders(includeAuth: true), - ); + /// with a simple retry mechanism for transient errors. + Future fetch( + Uri uri, { + int maxRetries = 3, + Duration initialDelay = const Duration(milliseconds: 250), + }) async { + int attempt = 0; + final random = math.Random(); - // Something wrong with our request - if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + Future wait(String reason) async { + final backoff = (initialDelay.inMilliseconds * math.pow(2, attempt - 1)).toInt(); + final jitter = random.nextInt((backoff * 0.25).toInt() + 1); // up to 25% jitter + final delay = backoff + jitter; + _logger.info('Retrying fetch for $uri, attempt $attempt (${delay}ms), reason: $reason'); + + await Future.delayed(Duration(milliseconds: delay)); } - // Process the response - return json.decode(utf8.decode(response.bodyBytes)) as dynamic; + while (true) { + try { + final response = await client + .get(uri, headers: getDefaultHeaders(includeAuth: true)) + .timeout(const Duration(seconds: 5)); + + if (response.statusCode >= 400) { + // Retry on server errors (5xx); e.g. 502 might be transient + if (response.statusCode >= 500 && attempt < maxRetries) { + attempt++; + await wait('status code ${response.statusCode}'); + continue; + } + throw WgerHttpException(response); + } + + return json.decode(utf8.decode(response.bodyBytes)) as dynamic; + } catch (e) { + final isRetryable = + e is SocketException || e is http.ClientException || e is TimeoutException; + if (isRetryable && attempt < maxRetries) { + attempt++; + await wait(e.toString()); + continue; + } + + rethrow; + } + } } /// Fetch and retrieve the overview list of objects, returns the JSON parsed response @@ -104,7 +144,7 @@ class WgerBaseProvider { // Something wrong with our request if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } return json.decode(response.body); @@ -120,7 +160,7 @@ class WgerBaseProvider { // Something wrong with our request if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } return json.decode(response.body); @@ -137,7 +177,7 @@ class WgerBaseProvider { // Something wrong with our request if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } return response; } diff --git a/lib/providers/body_weight.dart b/lib/providers/body_weight.dart index dddb8cec..cc93f7c3 100644 --- a/lib/providers/body_weight.dart +++ b/lib/providers/body_weight.dart @@ -18,7 +18,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/models/body_weight/weight_entry.dart'; import 'package:wger/providers/base_provider.dart'; @@ -115,7 +115,7 @@ class BodyWeightProvider with ChangeNotifier { if (response.statusCode >= 400) { _entries.insert(existingEntryIndex, existingWeightEntry); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } } } diff --git a/lib/providers/exercises.dart b/lib/providers/exercises.dart index 5ab3d70f..4e87cf16 100644 --- a/lib/providers/exercises.dart +++ b/lib/providers/exercises.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -22,9 +22,9 @@ import 'dart:convert'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/core/locator.dart'; import 'package:wger/database/exercises/exercise_database.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/models/exercises/category.dart'; @@ -234,7 +234,12 @@ class ExercisesProvider with ChangeNotifier { Future fetchAndSetCategoriesFromApi() async { _logger.info('Loading exercise categories from API'); - final categories = await baseProvider.fetchPaginated(baseProvider.makeUrl(categoriesUrlPath)); + final categories = await baseProvider.fetchPaginated( + baseProvider.makeUrl( + categoriesUrlPath, + query: {'limit': API_MAX_PAGE_SIZE}, + ), + ); for (final category in categories) { _categories.add(ExerciseCategory.fromJson(category)); } @@ -242,7 +247,12 @@ class ExercisesProvider with ChangeNotifier { Future fetchAndSetMusclesFromApi() async { _logger.info('Loading muscles from API'); - final muscles = await baseProvider.fetchPaginated(baseProvider.makeUrl(musclesUrlPath)); + final muscles = await baseProvider.fetchPaginated( + baseProvider.makeUrl( + musclesUrlPath, + query: {'limit': API_MAX_PAGE_SIZE}, + ), + ); for (final muscle in muscles) { _muscles.add(Muscle.fromJson(muscle)); @@ -251,7 +261,12 @@ class ExercisesProvider with ChangeNotifier { Future fetchAndSetEquipmentsFromApi() async { _logger.info('Loading equipment from API'); - final equipments = await baseProvider.fetchPaginated(baseProvider.makeUrl(equipmentUrlPath)); + final equipments = await baseProvider.fetchPaginated( + baseProvider.makeUrl( + equipmentUrlPath, + query: {'limit': API_MAX_PAGE_SIZE}, + ), + ); for (final equipment in equipments) { _equipment.add(Equipment.fromJson(equipment)); @@ -261,7 +276,12 @@ class ExercisesProvider with ChangeNotifier { Future fetchAndSetLanguagesFromApi() async { _logger.info('Loading languages from API'); - final languageData = await baseProvider.fetchPaginated(baseProvider.makeUrl(languageUrlPath)); + final languageData = await baseProvider.fetchPaginated( + baseProvider.makeUrl( + languageUrlPath, + query: {'limit': API_MAX_PAGE_SIZE}, + ), + ); for (final language in languageData) { _languages.add(Language.fromJson(language)); diff --git a/lib/providers/gym_log_state.dart b/lib/providers/gym_log_state.dart new file mode 100644 index 00000000..96604c5b --- /dev/null +++ b/lib/providers/gym_log_state.dart @@ -0,0 +1,46 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:wger/models/workouts/log.dart'; + +part 'gym_log_state.g.dart'; + +@Riverpod(keepAlive: true) +class GymLogNotifier extends _$GymLogNotifier { + final _logger = Logger('GymLogNotifier'); + + @override + Log? build() { + _logger.finer('Initializing GymLogNotifier'); + return null; + } + + void setLog(Log newLog) { + state = newLog; + } + + void setWeight(num weight) { + state = state?.copyWith(weight: weight); + } + + void setRepetitions(num repetitions) { + state = state?.copyWith(repetitions: repetitions); + } +} diff --git a/lib/providers/gym_log_state.g.dart b/lib/providers/gym_log_state.g.dart new file mode 100644 index 00000000..b2d45a45 --- /dev/null +++ b/lib/providers/gym_log_state.g.dart @@ -0,0 +1,55 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'gym_log_state.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(GymLogNotifier) +final gymLogProvider = GymLogNotifierProvider._(); + +final class GymLogNotifierProvider extends $NotifierProvider { + GymLogNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'gymLogProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$gymLogNotifierHash(); + + @$internal + @override + GymLogNotifier create() => GymLogNotifier(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Log? value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$gymLogNotifierHash() => r'4523975eeeaacceca4e86fb2e4ddd9a42c263d8e'; + +abstract class _$GymLogNotifier extends $Notifier { + Log? build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element as $ClassProviderElement, Log?, Object?, Object?>; + element.handleCreate(ref, build); + } +} diff --git a/lib/providers/gym_state.dart b/lib/providers/gym_state.dart index cec04d8f..882df366 100644 --- a/lib/providers/gym_state.dart +++ b/lib/providers/gym_state.dart @@ -1,3 +1,21 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import 'package:clock/clock.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -7,8 +25,10 @@ import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/helpers/uuid.dart'; import 'package:wger/models/exercises/exercise.dart'; import 'package:wger/models/workouts/day_data.dart'; +import 'package:wger/models/workouts/log.dart'; import 'package:wger/models/workouts/routine.dart'; import 'package:wger/models/workouts/set_config_data.dart'; +import 'package:wger/providers/gym_log_state.dart'; part 'gym_state.g.dart'; @@ -113,7 +133,11 @@ class SlotPageEntry { this.setConfigData, this.logDone = false, String? uuid, - }) : uuid = uuid ?? uuidV4(); + }) : assert( + type != SlotPageType.log || setConfigData != null, + 'You need to set setConfigData for SlotPageType.log', + ), + uuid = uuid ?? uuidV4(); SlotPageEntry copyWith({ String? uuid, @@ -463,7 +487,7 @@ class GymStateNotifier extends _$GymStateNotifier { pages.add(PageEntry(type: PageType.workoutSummary, pageIndex: pageIndex + 1)); state = state.copyWith(pages: pages); - // _logger.finer(readPageStructure()); + // print(readPageStructure()); _logger.finer('Initialized ${state.pages.length} pages'); } @@ -555,6 +579,17 @@ class GymStateNotifier extends _$GymStateNotifier { void setCurrentPage(int page) { state = state.copyWith(currentPage: page); + + // Ensure that there is a log entry for the current slot entry + final slotEntryPage = state.getSlotEntryPageByIndex(); + if (slotEntryPage == null || slotEntryPage.setConfigData == null) { + return; + } + + final log = Log.fromSetConfigData(slotEntryPage.setConfigData!); + log.routineId = state.routine.id!; + log.iteration = state.iteration; + ref.read(gymLogProvider.notifier).setLog(log); } void setShowExercisePages(bool value) { diff --git a/lib/providers/gym_state.g.dart b/lib/providers/gym_state.g.dart index 3a858e5e..4239a549 100644 --- a/lib/providers/gym_state.g.dart +++ b/lib/providers/gym_state.g.dart @@ -10,10 +10,10 @@ part of 'gym_state.dart'; // ignore_for_file: type=lint, type=warning @ProviderFor(GymStateNotifier) -const gymStateProvider = GymStateNotifierProvider._(); +final gymStateProvider = GymStateNotifierProvider._(); final class GymStateNotifierProvider extends $NotifierProvider { - const GymStateNotifierProvider._() + GymStateNotifierProvider._() : super( from: null, argument: null, @@ -40,14 +40,13 @@ final class GymStateNotifierProvider extends $NotifierProvider r'449bd80d3b534f68af4f0dbb8556c7f093f3b918'; +String _$gymStateNotifierHash() => r'3a0bb78e9f7e682ba93a40a73b170126b5eb5ca9'; abstract class _$GymStateNotifier extends $Notifier { GymModeState build(); @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -57,6 +56,6 @@ abstract class _$GymStateNotifier extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } diff --git a/lib/providers/measurement.dart b/lib/providers/measurement.dart index c961e938..6c0292f0 100644 --- a/lib/providers/measurement.dart +++ b/lib/providers/measurement.dart @@ -18,8 +18,8 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:wger/exceptions/http_exception.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/models/measurements/measurement_entry.dart'; diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index 11a82485..3ec50f80 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -21,10 +21,10 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/core/locator.dart'; import 'package:wger/database/ingredients/ingredients_database.dart'; -import 'package:wger/exceptions/http_exception.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/models/nutrition/ingredient_image.dart'; @@ -220,7 +220,7 @@ class NutritionPlansProvider with ChangeNotifier { if (response.statusCode >= 400) { _plans.insert(existingPlanIndex, existingPlan); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } //existingPlan = null; } @@ -263,7 +263,7 @@ class NutritionPlansProvider with ChangeNotifier { if (response.statusCode >= 400) { plan.meals.insert(mealIndex, existingMeal); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } } @@ -293,7 +293,7 @@ class NutritionPlansProvider with ChangeNotifier { if (response.statusCode >= 400) { meal.mealItems.insert(mealItemIndex, existingMealItem); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } } diff --git a/lib/providers/routines.dart b/lib/providers/routines.dart index ec8e53b9..67210c40 100644 --- a/lib/providers/routines.dart +++ b/lib/providers/routines.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -20,7 +20,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/models/exercises/exercise.dart'; @@ -374,7 +374,7 @@ class RoutinesProvider with ChangeNotifier { if (response.statusCode >= 400) { _routines.insert(routineIndex, routine); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } } diff --git a/lib/screens/add_exercise_screen.dart b/lib/screens/add_exercise_screen.dart index c91f74ef..9d94f5d2 100644 --- a/lib/screens/add_exercise_screen.dart +++ b/lib/screens/add_exercise_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/core/wide_screen_wrapper.dart'; -import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart index 902018df..aaa55e46 100644 --- a/lib/screens/auth_screen.dart +++ b/lib/screens/auth_screen.dart @@ -19,7 +19,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/screens/measurement_entries_screen.dart b/lib/screens/measurement_entries_screen.dart index 688bd868..3f30ed3e 100644 --- a/lib/screens/measurement_entries_screen.dart +++ b/lib/screens/measurement_entries_screen.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -18,8 +18,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/core/wide_screen_wrapper.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/providers/measurement.dart'; import 'package:wger/screens/form_screen.dart'; import 'package:wger/widgets/measurements/entries.dart'; @@ -38,7 +40,19 @@ class MeasurementEntriesScreen extends StatelessWidget { @override Widget build(BuildContext context) { final categoryId = ModalRoute.of(context)!.settings.arguments as int; - final category = Provider.of(context).findCategoryById(categoryId); + final provider = Provider.of(context); + MeasurementCategory? category; + + try { + category = provider.findCategoryById(categoryId); + } on NoSuchEntryException { + Future.microtask(() { + if (context.mounted && Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + }); + return const SizedBox(); // Return empty widget until pop happens + } return Scaffold( appBar: AppBar( @@ -65,7 +79,7 @@ class MeasurementEntriesScreen extends StatelessWidget { builder: (BuildContext contextDialog) { return AlertDialog( content: Text( - AppLocalizations.of(context).confirmDelete(category.name), + AppLocalizations.of(context).confirmDelete(category!.name), ), actions: [ TextButton( @@ -84,11 +98,12 @@ class MeasurementEntriesScreen extends StatelessWidget { Provider.of( context, listen: false, - ).deleteCategory(category.id!); - + ).deleteCategory(category!.id!); // Close the popup Navigator.of(contextDialog).pop(); + Navigator.of(context).pop(); // Exit detail screen + // and inform the user ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -138,7 +153,7 @@ class MeasurementEntriesScreen extends StatelessWidget { body: WidescreenWrapper( child: SingleChildScrollView( child: Consumer( - builder: (context, provider, child) => EntriesList(category), + builder: (context, provider, child) => EntriesList(category!), ), ), ), diff --git a/lib/widgets/dashboard/calendar.dart b/lib/widgets/dashboard/calendar.dart index e7175fd1..2809734c 100644 --- a/lib/widgets/dashboard/calendar.dart +++ b/lib/widgets/dashboard/calendar.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -134,33 +134,29 @@ class _DashboardCalendarWidgetState extends State // Process workout sessions final routinesProvider = context.read(); - await routinesProvider.fetchSessionData().then((sessions) { - for (final session in sessions) { - final date = DateFormatLists.format(session.date); - if (!_events.containsKey(date)) { - _events[date] = []; - } - var time = ''; - time = '(${timeToString(session.timeStart)} - ${timeToString(session.timeEnd)})'; - - // Add events to lists - _events[date]?.add( - Event( - EventType.session, - '${i18n.impression}: ${session.impressionAsString(context)} $time', - ), - ); - } - }); + final sessions = await routinesProvider.fetchSessionData(); if (!mounted) { return; } + for (final session in sessions) { + final date = DateFormatLists.format(session.date); + _events.putIfAbsent(date, () => []); + + final time = (session.timeStart != null && session.timeEnd != null) + ? '(${timeToString(session.timeStart)} - ${timeToString(session.timeEnd)})' + : ''; + + _events[date]?.add( + Event( + EventType.session, + '${i18n.impression}: ${session.impressionAsString(context)}${time.isNotEmpty ? ' $time' : ''}', + ), + ); + } + // Process nutritional plans - final NutritionPlansProvider nutritionProvider = Provider.of( - context, - listen: false, - ); + final nutritionProvider = context.read(); for (final plan in nutritionProvider.items) { for (final entry in plan.logEntriesValues.entries) { final date = DateFormatLists.format(entry.key); diff --git a/lib/widgets/exercises/videos.dart b/lib/widgets/exercises/videos.dart index 8f38da56..18d76ef9 100644 --- a/lib/widgets/exercises/videos.dart +++ b/lib/widgets/exercises/videos.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -20,6 +20,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; import 'package:video_player/video_player.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/models/exercises/video.dart'; @@ -33,9 +34,10 @@ class ExerciseVideoWidget extends StatefulWidget { } class _ExerciseVideoWidgetState extends State { + final logger = Logger('ExerciseVideoWidgetState'); + late VideoPlayerController _controller; bool hasError = false; - final logger = Logger('ExerciseVideoWidgetState'); @override void initState() { @@ -66,10 +68,14 @@ class _ExerciseVideoWidgetState extends State { @override Widget build(BuildContext context) { return hasError - ? const GeneralErrorsWidget( - [ - 'An error happened while loading the video. If you can, please check the application logs.', - ], + ? FormHttpErrorsWidget( + WgerHttpException.fromMap( + const { + 'error': + 'An error happened while loading the video. If you can, ' + 'please check the application logs.', + }, + ), ) : _controller.value.isInitialized ? AspectRatio( diff --git a/lib/widgets/routines/forms/day.dart b/lib/widgets/routines/forms/day.dart index a0ae3c25..6af0dc10 100644 --- a/lib/widgets/routines/forms/day.dart +++ b/lib/widgets/routines/forms/day.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/widgets/routines/forms/reps_unit.dart b/lib/widgets/routines/forms/reps_unit.dart index 70e53c3b..f19d7e9f 100644 --- a/lib/widgets/routines/forms/reps_unit.dart +++ b/lib/widgets/routines/forms/reps_unit.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -29,7 +29,7 @@ class RepetitionUnitInputWidget extends StatefulWidget { late int? selectedRepetitionUnit; final ValueChanged onChanged; - RepetitionUnitInputWidget(initialValue, {required this.onChanged}) { + RepetitionUnitInputWidget(int? initialValue, {super.key, required this.onChanged}) { selectedRepetitionUnit = initialValue; } @@ -47,7 +47,7 @@ class _RepetitionUnitInputWidgetState extends State { : null; return DropdownButtonFormField( - value: selectedWeightUnit, + initialValue: selectedWeightUnit, decoration: InputDecoration( labelText: AppLocalizations.of(context).repetitionUnit, ), diff --git a/lib/widgets/routines/forms/rir.dart b/lib/widgets/routines/forms/rir.dart index 22c95753..da406659 100644 --- a/lib/widgets/routines/forms/rir.dart +++ b/lib/widgets/routines/forms/rir.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -30,7 +30,7 @@ class RiRInputWidget extends StatefulWidget { static const SLIDER_START = -0.5; - RiRInputWidget(this._initialValue, {required this.onChanged}) { + RiRInputWidget(this._initialValue, {super.key, required this.onChanged}) { _logger.finer('Initializing with initial value: $_initialValue'); } diff --git a/lib/widgets/routines/forms/routine.dart b/lib/widgets/routines/forms/routine.dart index bbdc9bb2..cb6d3b01 100644 --- a/lib/widgets/routines/forms/routine.dart +++ b/lib/widgets/routines/forms/routine.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/widgets/routines/forms/session.dart b/lib/widgets/routines/forms/session.dart index 231adc73..b09cbbef 100644 --- a/lib/widgets/routines/forms/session.dart +++ b/lib/widgets/routines/forms/session.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -20,7 +20,7 @@ import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/helpers/json.dart'; @@ -100,6 +100,7 @@ class _SessionFormState extends State { children: [ errorMessage, ToggleButtons( + key: const ValueKey('impression-toggle-buttons'), renderBorder: false, onPressed: (int index) { setState(() { diff --git a/lib/widgets/routines/forms/slot.dart b/lib/widgets/routines/forms/slot.dart index 59ab4aa9..f392d997 100644 --- a/lib/widgets/routines/forms/slot.dart +++ b/lib/widgets/routines/forms/slot.dart @@ -18,7 +18,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/day.dart'; diff --git a/lib/widgets/routines/forms/slot_entry.dart b/lib/widgets/routines/forms/slot_entry.dart index 94ccd7f5..a9580004 100644 --- a/lib/widgets/routines/forms/slot_entry.dart +++ b/lib/widgets/routines/forms/slot_entry.dart @@ -19,7 +19,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/widgets/routines/forms/weight_unit.dart b/lib/widgets/routines/forms/weight_unit.dart index 8afb6784..751c6e40 100644 --- a/lib/widgets/routines/forms/weight_unit.dart +++ b/lib/widgets/routines/forms/weight_unit.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -29,7 +29,7 @@ class WeightUnitInputWidget extends StatefulWidget { late int? selectedWeightUnit; final ValueChanged onChanged; - WeightUnitInputWidget(int? initialValue, {required this.onChanged}) { + WeightUnitInputWidget(int? initialValue, {super.key, required this.onChanged}) { selectedWeightUnit = initialValue; } @@ -47,7 +47,7 @@ class _WeightUnitInputWidgetState extends State { : null; return DropdownButtonFormField( - value: selectedWeightUnit, + initialValue: selectedWeightUnit, decoration: InputDecoration(labelText: AppLocalizations.of(context).weightUnit), onChanged: (WeightUnit? newValue) { setState(() { diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index d4fa2575..088836d6 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -15,17 +15,19 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart' as provider; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/log.dart'; import 'package:wger/models/workouts/set_config_data.dart'; import 'package:wger/models/workouts/slot_entry.dart'; +import 'package:wger/providers/gym_log_state.dart'; import 'package:wger/providers/gym_state.dart'; import 'package:wger/providers/plate_weights.dart'; import 'package:wger/providers/routines.dart'; @@ -38,60 +40,37 @@ import 'package:wger/widgets/routines/forms/weight_unit.dart'; import 'package:wger/widgets/routines/gym_mode/navigation.dart'; import 'package:wger/widgets/routines/plate_calculator.dart'; -class LogPage extends ConsumerStatefulWidget { +class LogPage extends ConsumerWidget { final _logger = Logger('LogPage'); final PageController _controller; - LogPage(this._controller); - - @override - _LogPageState createState() => _LogPageState(); -} - -class _LogPageState extends ConsumerState { final GlobalKey<_LogFormWidgetState> _logFormKey = GlobalKey<_LogFormWidgetState>(); - late FocusNode focusNode; - @override - void initState() { - super.initState(); - focusNode = FocusNode(); - } - - @override - void dispose() { - focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final state = ref.watch(gymStateProvider); + final gymState = ref.watch(gymStateProvider); + final languageCode = Localizations.localeOf(context).languageCode; - final page = state.getPageByIndex(); + final page = gymState.getPageByIndex(); if (page == null) { - widget._logger.info( - 'getPageByIndex for ${state.currentPage} returned null, showing empty container.', + _logger.info( + 'getPageByIndex for ${gymState.currentPage} returned null, showing empty container.', ); return Container(); } - final slotEntryPage = state.getSlotEntryPageByIndex(); + final slotEntryPage = gymState.getSlotEntryPageByIndex(); if (slotEntryPage == null) { - widget._logger.info( - 'getSlotPageByIndex for ${state.currentPage} returned null, showing empty container', + _logger.info( + 'getSlotPageByIndex for ${gymState.currentPage} returned null, showing empty container', ); return Container(); } - final setConfigData = slotEntryPage.setConfigData!; - final log = Log.fromSetConfigData(setConfigData) - ..routineId = state.routine.id! - ..iteration = state.iteration; + final log = ref.watch(gymLogProvider); // Mark done sets final decorationStyle = slotEntryPage.logDone @@ -101,8 +80,9 @@ class _LogPageState extends ConsumerState { return Column( children: [ NavigationHeader( - log.exercise.getTranslation(Localizations.localeOf(context).languageCode).name, - widget._controller, + log!.exercise.getTranslation(languageCode).name, + _controller, + key: const ValueKey('log-page-navigation-header'), ), Container( @@ -148,16 +128,9 @@ class _LogPageState extends ConsumerState { Text(slotEntryPage.setConfigData!.comment, textAlign: TextAlign.center), const SizedBox(height: 10), Expanded( - child: (state.routine.filterLogsByExercise(log.exercise.id!).isNotEmpty) + child: (gymState.routine.filterLogsByExercise(log.exerciseId).isNotEmpty) ? LogsPastLogsWidget( - log: log, - pastLogs: state.routine.filterLogsByExercise(log.exercise.id!), - onCopy: (pastLog) { - _logFormKey.currentState?.copyFromPastLog(pastLog); - }, - setStateCallback: (fn) { - setState(fn); - }, + pastLogs: gymState.routine.filterLogsByExercise(log.exerciseId), ) : Container(), ), @@ -170,16 +143,15 @@ class _LogPageState extends ConsumerState { child: Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: LogFormWidget( - key: _logFormKey, - controller: widget._controller, + controller: _controller, configData: setConfigData, - log: log, - focusNode: focusNode, + // log: log!, + key: _logFormKey, ), ), ), ), - NavigationFooter(widget._controller), + NavigationFooter(_controller), ], ); } @@ -239,68 +211,62 @@ class LogsPlatesWidget extends ConsumerWidget { } } -class LogsRepsWidget extends StatelessWidget { - final TextEditingController controller; - final SetConfigData configData; - final FocusNode focusNode; - final Log log; - final void Function(VoidCallback fn) setStateCallback; - +class LogsRepsWidget extends ConsumerWidget { final _logger = Logger('LogsRepsWidget'); + final num valueChange; + LogsRepsWidget({ super.key, - required this.controller, - required this.configData, - required this.focusNode, - required this.log, - required this.setStateCallback, - }); + num? valueChange, + }) : valueChange = valueChange ?? 1; @override - Widget build(BuildContext context) { - final repsValueChange = configData.repetitionsRounding ?? 1; + Widget build(BuildContext context, WidgetRef ref) { final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString()); - final i18n = AppLocalizations.of(context); + final logNotifier = ref.read(gymLogProvider.notifier); + final log = ref.watch(gymLogProvider); + + final currentReps = log?.repetitions; + final repText = currentReps != null ? numberFormat.format(currentReps) : ''; + return Row( children: [ + // "Quick-remove" button IconButton( icon: const Icon(Icons.remove, color: Colors.black), onPressed: () { - final currentValue = numberFormat.tryParse(controller.text) ?? 0; - final newValue = currentValue - repsValueChange; - if (newValue >= 0) { - setStateCallback(() { - log.repetitions = newValue; - controller.text = numberFormat.format(newValue); - }); + final base = currentReps ?? 0; + final newValue = base - valueChange; + if (newValue >= 0 && log != null) { + logNotifier.setRepetitions(newValue); } }, ), + + // Text field Expanded( child: TextFormField( decoration: InputDecoration(labelText: i18n.repetitions), enabled: true, - controller: controller, + key: ValueKey('reps-field-$repText'), + initialValue: repText, keyboardType: textInputTypeDecimal, - focusNode: focusNode, onChanged: (value) { try { final newValue = numberFormat.parse(value); - setStateCallback(() { - log.repetitions = newValue; - }); + logNotifier.setRepetitions(newValue); } on FormatException catch (error) { - _logger.fine('Error parsing repetitions: $error'); + _logger.finer('Error parsing repetitions: $error'); } }, onSaved: (newValue) { - _logger.info('Saving new reps value: $newValue'); - setStateCallback(() { - log.repetitions = numberFormat.parse(newValue!); - }); + if (newValue == null || log == null) { + return; + } + logNotifier.setRepetitions(numberFormat.parse(newValue)); }, validator: (value) { if (numberFormat.tryParse(value ?? '') == null) { @@ -310,19 +276,15 @@ class LogsRepsWidget extends StatelessWidget { }, ), ), + + // "Quick-add" button IconButton( icon: const Icon(Icons.add, color: Colors.black), onPressed: () { - final value = controller.text.isNotEmpty ? controller.text : '0'; - - try { - final newValue = numberFormat.parse(value) + repsValueChange; - setStateCallback(() { - log.repetitions = newValue; - controller.text = numberFormat.format(newValue); - }); - } on FormatException catch (error) { - _logger.fine('Error parsing reps during quick-add: $error'); + final base = currentReps ?? 0; + final newValue = base + valueChange; + if (newValue >= 0 && log != null) { + logNotifier.setRepetitions(newValue); } }, ), @@ -332,76 +294,62 @@ class LogsRepsWidget extends StatelessWidget { } class LogsWeightWidget extends ConsumerWidget { - final TextEditingController controller; - final SetConfigData configData; - final FocusNode focusNode; - final Log log; - final void Function(VoidCallback fn) setStateCallback; - final _logger = Logger('LogsWeightWidget'); + final num valueChange; + LogsWeightWidget({ super.key, - required this.controller, - required this.configData, - required this.focusNode, - required this.log, - required this.setStateCallback, - }); + num? valueChange, + }) : valueChange = valueChange ?? 1.25; @override Widget build(BuildContext context, WidgetRef ref) { - final weightValueChange = configData.weightRounding ?? 1.25; final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString()); final i18n = AppLocalizations.of(context); + final plateProvider = ref.read(plateCalculatorProvider.notifier); + final logProvider = ref.read(gymLogProvider.notifier); + final log = ref.watch(gymLogProvider); + + final currentWeight = log?.weight; + final weightText = currentWeight != null ? numberFormat.format(currentWeight) : ''; + return Row( children: [ IconButton( + // "Quick-remove" button icon: const Icon(Icons.remove, color: Colors.black), onPressed: () { - try { - final newValue = numberFormat.parse(controller.text) - weightValueChange; - if (newValue > 0) { - setStateCallback(() { - log.weight = newValue; - controller.text = numberFormat.format(newValue); - ref - .read(plateCalculatorProvider.notifier) - .setWeight( - controller.text == '' ? 0 : newValue, - ); - }); - } - } on FormatException catch (error) { - _logger.fine('Error parsing weight during quick-remove: $error'); + final base = currentWeight ?? 0; + final newValue = base - valueChange; + if (newValue >= 0 && log != null) { + logProvider.setWeight(newValue); } }, ), + + // Text field Expanded( child: TextFormField( + key: ValueKey('weight-field-$weightText'), decoration: InputDecoration(labelText: i18n.weight), - controller: controller, + initialValue: weightText, keyboardType: textInputTypeDecimal, onChanged: (value) { try { final newValue = numberFormat.parse(value); - setStateCallback(() { - log.weight = newValue; - ref - .read(plateCalculatorProvider.notifier) - .setWeight( - controller.text == '' ? 0 : numberFormat.parse(controller.text), - ); - }); + plateProvider.setWeight(newValue); + logProvider.setWeight(newValue); } on FormatException catch (error) { - _logger.fine('Error parsing weight: $error'); + _logger.finer('Error parsing weight: $error'); } }, onSaved: (newValue) { - setStateCallback(() { - log.weight = numberFormat.parse(newValue!); - }); + if (newValue == null || log == null) { + return; + } + logProvider.setWeight(numberFormat.parse(newValue)); }, validator: (value) { if (numberFormat.tryParse(value ?? '') == null) { @@ -411,24 +359,15 @@ class LogsWeightWidget extends ConsumerWidget { }, ), ), + + // "Quick-add" button IconButton( icon: const Icon(Icons.add, color: Colors.black), onPressed: () { - final value = controller.text.isNotEmpty ? controller.text : '0'; - - try { - final newValue = numberFormat.parse(value) + weightValueChange; - setStateCallback(() { - log.weight = newValue; - controller.text = numberFormat.format(newValue); - ref - .read(plateCalculatorProvider.notifier) - .setWeight( - controller.text == '' ? 0 : newValue, - ); - }); - } on FormatException catch (error) { - _logger.fine('Error parsing weight during quick-add: $error'); + final base = currentWeight ?? 0; + final newValue = base + valueChange; + if (log != null) { + logProvider.setWeight(newValue); } }, ), @@ -437,22 +376,19 @@ class LogsWeightWidget extends ConsumerWidget { } } -class LogsPastLogsWidget extends StatelessWidget { - final Log log; +class LogsPastLogsWidget extends ConsumerWidget { final List pastLogs; - final void Function(Log pastLog) onCopy; - final void Function(VoidCallback fn) setStateCallback; const LogsPastLogsWidget({ super.key, - required this.log, required this.pastLogs, - required this.onCopy, - required this.setStateCallback, }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final logProvider = ref.read(gymLogProvider.notifier); + final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode); + return Container( padding: const EdgeInsets.symmetric(vertical: 8), child: ListView( @@ -466,25 +402,16 @@ class LogsPastLogsWidget extends StatelessWidget { return ListTile( key: ValueKey('past-log-${pastLog.id}'), title: Text(pastLog.repTextNoNl(context)), - subtitle: Text( - DateFormat.yMd(Localizations.localeOf(context).languageCode).format(pastLog.date), - ), + subtitle: Text(dateFormat.format(pastLog.date)), trailing: const Icon(Icons.copy), onTap: () { - setStateCallback(() { - log.rir = pastLog.rir; - log.repetitionUnit = pastLog.repetitionsUnitObj; - log.weightUnit = pastLog.weightUnitObj; - - onCopy(pastLog); - - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context).dataCopied), - ), - ); - }); + logProvider.setLog(pastLog); + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context).dataCopied), + ), + ); }, contentPadding: const EdgeInsets.symmetric(horizontal: 40), ); @@ -500,15 +427,11 @@ class LogFormWidget extends ConsumerStatefulWidget { final PageController controller; final SetConfigData configData; - final Log log; - final FocusNode focusNode; LogFormWidget({ super.key, required this.controller, required this.configData, - required this.log, - required this.focusNode, }); @override @@ -519,61 +442,11 @@ class _LogFormWidgetState extends ConsumerState { final _form = GlobalKey(); var _detailed = false; bool _isSaving = false; - late Log _log; - - late final TextEditingController _repetitionsController; - late final TextEditingController _weightController; - - @override - void initState() { - super.initState(); - - _log = widget.log; - _repetitionsController = TextEditingController(); - _weightController = TextEditingController(); - - WidgetsBinding.instance.addPostFrameCallback((_) { - final locale = Localizations.localeOf(context).toString(); - final numberFormat = NumberFormat.decimalPattern(locale); - - if (widget.configData.repetitions != null) { - _repetitionsController.text = numberFormat.format(widget.configData.repetitions); - } - - if (widget.configData.weight != null) { - _weightController.text = numberFormat.format(widget.configData.weight); - } - }); - } - - @override - void dispose() { - _repetitionsController.dispose(); - _weightController.dispose(); - super.dispose(); - } - - void copyFromPastLog(Log pastLog) { - final locale = Localizations.localeOf(context).toString(); - final numberFormat = NumberFormat.decimalPattern(locale); - - setState(() { - _repetitionsController.text = pastLog.repetitions != null - ? numberFormat.format(pastLog.repetitions) - : ''; - widget._logger.finer('Setting log repetitions to ${_repetitionsController.text}'); - - _weightController.text = pastLog.weight != null ? numberFormat.format(pastLog.weight) : ''; - widget._logger.finer('Setting log weight to ${_weightController.text}'); - - _log.rir = pastLog.rir; - widget._logger.finer('Setting log rir to ${_log.rir}'); - }); - } @override Widget build(BuildContext context) { final i18n = AppLocalizations.of(context); + final log = ref.watch(gymLogProvider); return Form( key: _form, @@ -590,25 +463,15 @@ class _LogFormWidgetState extends ConsumerState { children: [ Flexible( child: LogsRepsWidget( - controller: _repetitionsController, - configData: widget.configData, - focusNode: widget.focusNode, - log: _log, - setStateCallback: (fn) { - setState(fn); - }, + key: const ValueKey('logs-reps-widget'), + valueChange: widget.configData.repetitionsRounding, ), ), const SizedBox(width: 8), Flexible( child: LogsWeightWidget( - controller: _weightController, - configData: widget.configData, - focusNode: widget.focusNode, - log: _log, - setStateCallback: (fn) { - setState(fn); - }, + key: const ValueKey('logs-weight-widget'), + valueChange: widget.configData.weightRounding, ), ), ], @@ -619,19 +482,15 @@ class _LogFormWidgetState extends ConsumerState { children: [ Flexible( child: LogsRepsWidget( - controller: _repetitionsController, - configData: widget.configData, - focusNode: widget.focusNode, - log: _log, - setStateCallback: (fn) { - setState(fn); - }, + key: const ValueKey('logs-reps-widget'), + valueChange: widget.configData.repetitionsRounding, ), ), const SizedBox(width: 8), Flexible( child: RepetitionUnitInputWidget( - _log.repetitionsUnitId, + key: const ValueKey('repetition-unit-input-widget'), + log!.repetitionsUnitId, onChanged: (v) => {}, ), ), @@ -644,34 +503,31 @@ class _LogFormWidgetState extends ConsumerState { children: [ Flexible( child: LogsWeightWidget( - controller: _weightController, - configData: widget.configData, - focusNode: widget.focusNode, - log: _log, - setStateCallback: (fn) { - setState(fn); - }, + key: const ValueKey('logs-weight-widget'), + valueChange: widget.configData.weightRounding, ), ), const SizedBox(width: 8), Flexible( - child: WeightUnitInputWidget(_log.weightUnitId, onChanged: (v) => {}), + child: WeightUnitInputWidget( + log!.weightUnitId, + onChanged: (v) => {}, + key: const ValueKey('weight-unit-input-widget'), + ), ), const SizedBox(width: 8), ], ), if (_detailed) RiRInputWidget( - _log.rir, + key: const ValueKey('rir-input-widget'), + log!.rir, onChanged: (value) { - if (value == '') { - _log.rir = null; - } else { - _log.rir = num.parse(value); - } + log.rir = value == '' ? null : num.parse(value); }, ), SwitchListTile( + key: const ValueKey('units-switch'), dense: true, title: Text(i18n.setUnitsAndRir), value: _detailed, @@ -682,6 +538,7 @@ class _LogFormWidgetState extends ConsumerState { }, ), FilledButton( + key: const ValueKey('save-log-button'), onPressed: _isSaving ? null : () async { @@ -699,7 +556,7 @@ class _LogFormWidgetState extends ConsumerState { await provider.Provider.of( context, listen: false, - ).addLog(_log); + ).addLog(log!); final page = gymState.getSlotEntryPageByIndex()!; gymProvider.markSlotPageAsDone(page.uuid, isDone: true); diff --git a/lib/widgets/routines/gym_mode/navigation.dart b/lib/widgets/routines/gym_mode/navigation.dart index d2572b52..9639c3d0 100644 --- a/lib/widgets/routines/gym_mode/navigation.dart +++ b/lib/widgets/routines/gym_mode/navigation.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -28,7 +28,12 @@ class NavigationHeader extends StatelessWidget { final String _title; final bool showEndWorkoutButton; - const NavigationHeader(this._title, this._controller, {this.showEndWorkoutButton = true}); + const NavigationHeader( + this._title, + this._controller, { + this.showEndWorkoutButton = true, + super.key, + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/routines/gym_mode/session_page.dart b/lib/widgets/routines/gym_mode/session_page.dart index 51830cb5..f1789a87 100644 --- a/lib/widgets/routines/gym_mode/session_page.dart +++ b/lib/widgets/routines/gym_mode/session_page.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2025 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -15,59 +15,96 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ + import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:provider/provider.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/date.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/models/workouts/routine.dart'; import 'package:wger/models/workouts/session.dart'; import 'package:wger/providers/gym_state.dart'; +import 'package:wger/providers/routines.dart'; import 'package:wger/widgets/routines/forms/session.dart'; import 'package:wger/widgets/routines/gym_mode/navigation.dart'; -class SessionPage extends ConsumerWidget { +class SessionPage extends ConsumerStatefulWidget { final PageController _controller; const SessionPage(this._controller); @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(gymStateProvider); + ConsumerState createState() => _SessionPageState(); +} - final session = state.routine.sessions - .map((sessionApi) => sessionApi.session) - .firstWhere( - (session) => session.date.isSameDayAs(clock.now()), - orElse: () => WorkoutSession( - dayId: state.dayId, - routineId: state.routine.id, - impression: DEFAULT_IMPRESSION, - date: clock.now(), - timeStart: state.startTime, - timeEnd: TimeOfDay.fromDateTime(clock.now()), - ), - ); +class _SessionPageState extends ConsumerState { + late Future _initData; + late Routine _routine; + + @override + void initState() { + super.initState(); + _initData = _reloadRoutineData(); + } + + Future _reloadRoutineData() async { + final gymState = ref.read(gymStateProvider); + _routine = await context.read().fetchAndSetRoutineFull(gymState.routine.id!); + } + + @override + Widget build(BuildContext context) { + final i18n = AppLocalizations.of(context); + final gymState = ref.read(gymStateProvider); return Column( children: [ - NavigationHeader( - AppLocalizations.of(context).workoutSession, - _controller, - ), - Expanded(child: Container()), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: SessionForm( - state.routine.id, - onSaved: () => _controller.nextPage( - duration: DEFAULT_ANIMATION_DURATION, - curve: DEFAULT_ANIMATION_CURVE, - ), - session: session, + NavigationHeader(i18n.workoutSession, widget._controller), + Expanded( + child: FutureBuilder( + future: _initData, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else { + final session = _routine.sessions + .map((sessionApi) => sessionApi.session) + .firstWhere( + (s) => s.date.isSameDayAs(clock.now()), + orElse: () => WorkoutSession( + dayId: gymState.dayId, + date: clock.now(), + routineId: gymState.routine.id, + timeStart: gymState.startTime, + timeEnd: TimeOfDay.fromDateTime(clock.now()), + ), + ); + + return Column( + children: [ + Expanded(child: Container()), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: SessionForm( + gymState.routine.id, + onSaved: () => widget._controller.nextPage( + duration: DEFAULT_ANIMATION_DURATION, + curve: DEFAULT_ANIMATION_CURVE, + ), + session: session, + ), + ), + ], + ); + } + }, ), ), - NavigationFooter(_controller), + NavigationFooter(widget._controller), ], ); } diff --git a/pubspec.lock b/pubspec.lock index 3a7bf2ed..77750a63 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: analyzer - sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 url: "https://pub.dev" source: hosted - version: "8.4.0" + version: "8.4.1" analyzer_buffer: dependency: transitive description: @@ -25,14 +25,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.11" - analyzer_plugin: - dependency: transitive - description: - name: analyzer_plugin - sha256: "08cfefa90b4f4dd3b447bda831cecf644029f9f8e22820f6ee310213ebe2dd53" - url: "https://pub.dev" - source: hosted - version: "0.13.10" archive: dependency: transitive description: @@ -69,10 +61,10 @@ packages: dependency: transitive description: name: build - sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413 + sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.4" build_config: dependency: transitive description: @@ -93,10 +85,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057" + sha256: b4d854962a32fd9f8efc0b76f98214790b833af8b2e9b2df6bfc927c0415a072 url: "https://pub.dev" source: hosted - version: "2.10.4" + version: "2.10.5" built_collection: dependency: transitive description: @@ -109,10 +101,10 @@ packages: dependency: transitive description: name: built_value - sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" + sha256: "120df83d4a4ce6bed06ad653c0a3e85737e0f66664f31e17a55136ff5a738cde" url: "https://pub.dev" source: hosted - version: "8.12.1" + version: "8.12.2" camera: dependency: transitive description: @@ -125,18 +117,18 @@ packages: dependency: transitive description: name: camera_android_camerax - sha256: "1f1d1ff65223c59018d58bdac5211417c2af60bcb469c9d26f928dd412eb91cf" + sha256: bc7a96998258adddd0b653dd693b0874537707d58b0489708f2a646e4f124246 url: "https://pub.dev" source: hosted - version: "0.6.24+3" + version: "0.6.27" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "035b90c1e33c2efad7548f402572078f6e514d4f82be0a315cd6c6af7e855aa8" + sha256: "087a9fadef20325cb246b4c13344a3ce8e408acfc3e0c665ebff0ec3144d7163" url: "https://pub.dev" source: hosted - version: "0.9.22+6" + version: "0.9.22+8" camera_platform_interface: dependency: transitive description: @@ -149,10 +141,10 @@ packages: dependency: transitive description: name: camera_web - sha256: "77e53acb64d9de8917424eeb32b5c7c73572d1e00954bbf54a1e609d79a751a2" + sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7" url: "https://pub.dev" source: hosted - version: "0.3.5+1" + version: "0.3.5+3" carousel_slider: dependency: "direct main" description: @@ -197,10 +189,10 @@ packages: dependency: "direct dev" description: name: cider - sha256: dfff70e9324f99e315857c596c31f54cb7380cfa20dfdfdca11a3631e05b7d3e + sha256: "455e3549bd1d21708326985702703345245acd3d7a2ac485de4183affb414a2c" url: "https://pub.dev" source: hosted - version: "0.2.8" + version: "0.2.9" cli_config: dependency: transitive description: @@ -229,10 +221,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.11.1" collection: dependency: "direct main" description: @@ -289,22 +281,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" - custom_lint_core: - dependency: transitive - description: - name: custom_lint_core - sha256: "85b339346154d5646952d44d682965dfe9e12cae5febd706f0db3aa5010d6423" - url: "https://pub.dev" - source: hosted - version: "0.8.1" - custom_lint_visitor: - dependency: transitive - description: - name: custom_lint_visitor - sha256: "91f2a81e9f0abb4b9f3bb529f78b6227ce6050300d1ae5b1e2c69c66c7a566d8" - url: "https://pub.dev" - source: hosted - version: "1.0.0+8.4.0" dart_style: dependency: transitive description: @@ -317,26 +293,26 @@ packages: dependency: "direct main" description: name: drift - sha256: "83290a32ae006a7535c5ecf300722cb77177250d9df4ee2becc5fa8a36095114" + sha256: "5ea2f718558c0b31d4b8c36a3d8e5b7016f1265f46ceb5a5920e16117f0c0d6a" url: "https://pub.dev" source: hosted - version: "2.29.0" + version: "2.30.1" drift_dev: dependency: "direct dev" description: name: drift_dev - sha256: "6019f827544e77524ffd5134ae0cb75dfd92ef5ef3e269872af92840c929cd43" + sha256: "892dfb5d69d9e604bdcd102a9376de8b41768cf7be93fd26b63cfc4d8f91ad5f" url: "https://pub.dev" source: hosted - version: "2.29.0" + version: "2.30.1" equatable: dependency: "direct main" description: name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" fake_async: dependency: transitive description: @@ -349,10 +325,10 @@ packages: dependency: transitive description: name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" file: dependency: transitive description: @@ -516,10 +492,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "9e2d6907f12cc7d23a846847615941bddee8709bf2bfd274acdf5e80bcf22fde" + sha256: "38ec6c303e2c83ee84512f5fc2a82ae311531021938e63d7137eccc107bf3c02" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.1.0" flutter_staggered_grid_view: dependency: "direct main" description: @@ -667,10 +643,10 @@ packages: dependency: transitive description: name: image - sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" url: "https://pub.dev" source: hosted - version: "4.5.4" + version: "4.7.2" image_picker: dependency: "direct main" description: @@ -699,10 +675,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "997d100ce1dda5b1ba4085194c5e36c9f8a1fb7987f6a36ab677a344cd2dc986" + sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" url: "https://pub.dev" source: hosted - version: "0.8.13+2" + version: "0.8.13+3" image_picker_linux: dependency: transitive description: @@ -880,10 +856,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: dac24d461418d363778d53198d9ac0510b9d073869f078450f195766ec48d05e + sha256: a45d1aa065b796922db7b9e7e7e45f921aed17adf3a8318a1f47097e7e695566 url: "https://pub.dev" source: hosted - version: "5.6.1" + version: "5.6.3" multi_select_flutter: dependency: "direct main" description: @@ -976,10 +952,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "97390a0719146c7c3e71b6866c34f1cde92685933165c1c671984390d2aca776" + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.5.1" path_provider_linux: dependency: transitive description: @@ -1144,50 +1120,50 @@ packages: dependency: transitive description: name: riverpod - sha256: c406de02bff19d920b832bddfb8283548bfa05ce41c59afba57ce643e116aa59 + sha256: "16ff608d21e8ea64364f2b7c049c94a02ab81668f78845862b6e88b71dd4935a" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.1.0" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: a0f68adb078b790faa3c655110a017f9a7b7b079a57bbd40f540e80dce5fcd29 + sha256: "947b05d04c52a546a2ac6b19ef2a54b08520ff6bdf9f23d67957a4c8df1c3bc0" url: "https://pub.dev" source: hosted - version: "1.0.0-dev.7" + version: "1.0.0-dev.8" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: "7230014155777fc31ba3351bc2cb5a3b5717b11bfafe52b1553cb47d385f8897" + sha256: cc1474bc2df55ec3c1da1989d139dcef22cd5e2bd78da382e867a69a8eca2e46 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "4.0.0" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "49894543a42cf7a9954fc4e7366b6d3cb2e6ec0fa07775f660afcdd92d097702" + sha256: e43b1537229cc8f487f09b0c20d15dba840acbadcf5fc6dad7ad5e8ab75950dc url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "4.0.0+1" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "46a46fd64659eff15f4638bbe19de43f9483f0e0bf024a9fb6b3582064bacc7b" + sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" url: "https://pub.dev" source: hosted - version: "2.4.17" + version: "2.4.18" shared_preferences_foundation: dependency: transitive description: @@ -1333,10 +1309,10 @@ packages: dependency: transitive description: name: sqlparser - sha256: "54eea43e36dd3769274c3108625f9ea1a382f8d2ac8b16f3e4589d9bd9b0e16c" + sha256: f52f5d5649dcc13ed198c4176ddef74bf6851c30f4f31603f1b37788695b93e2 url: "https://pub.dev" source: hosted - version: "0.42.0" + version: "0.43.0" stack_trace: dependency: transitive description: @@ -1497,14 +1473,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.5" - uuid: - dependency: transitive - description: - name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 - url: "https://pub.dev" - source: hosted - version: "4.5.2" vector_graphics: dependency: transitive description: @@ -1565,18 +1533,18 @@ packages: dependency: transitive description: name: video_player_android - sha256: "3f7ef3fb7b29f510e58f4d56b6ffbc3463b1071f2cf56e10f8d25f5b991ed85b" + sha256: ee4fd520b0cafa02e4a867a0f882092e727cdaa1a2d24762171e787f8a502b0a url: "https://pub.dev" source: hosted - version: "2.8.21" + version: "2.9.1" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "6bced1739cf1f96f03058118adb8ac0dd6f96aa1a1a6e526424ab92fd2a6a77d" + sha256: d1eb970495a76abb35e5fa93ee3c58bd76fb6839e2ddf2fbb636674f2b971dd4 url: "https://pub.dev" source: hosted - version: "2.8.7" + version: "2.8.9" video_player_platform_interface: dependency: transitive description: @@ -1605,10 +1573,10 @@ packages: dependency: transitive description: name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.1" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2b70b8e1..9738a313 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # - the version number is taken from the git tag vX.Y.Z # - the build number is computed by reading the last one from the play store # and increasing by one -version: 1.9.2+120 +version: 1.9.5+150 environment: sdk: '>=3.8.0 <4.0.0' @@ -36,8 +36,8 @@ dependencies: clock: ^1.1.2 collection: ^1.18.0 cupertino_icons: ^1.0.8 - drift: ^2.29.0 - equatable: ^2.0.7 + drift: ^2.30.0 + equatable: ^2.0.8 fl_chart: ^1.1.1 flex_color_scheme: ^8.3.1 flutter_html: ^3.0.0 @@ -66,8 +66,8 @@ dependencies: version: ^3.0.2 video_player: ^2.10.1 logging: ^1.3.0 - flutter_riverpod: ^3.0.3 - riverpod_annotation: ^3.0.3 + flutter_riverpod: ^3.1.0 + riverpod_annotation: ^4.0.0 dev_dependencies: flutter_test: @@ -75,7 +75,7 @@ dev_dependencies: integration_test: sdk: flutter build_runner: ^2.10.4 - cider: ^0.2.7 + cider: ^0.2.9 drift_dev: ^2.29.0 flutter_lints: ^6.0.0 freezed: ^3.2.0 @@ -83,7 +83,7 @@ dev_dependencies: mockito: ^5.4.4 network_image_mock: ^2.1.1 shared_preferences_platform_interface: ^2.0.0 - riverpod_generator: ^3.0.3 + riverpod_generator: ^4.0.0+1 # Script to read out unused translations #translations_cleaner: ^0.0.5 diff --git a/test/core/http_exception_test.dart b/test/core/http_exception_test.dart new file mode 100644 index 00000000..30ae455c --- /dev/null +++ b/test/core/http_exception_test.dart @@ -0,0 +1,107 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2020 - 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:wger/core/exceptions/http_exception.dart'; + +void main() { + group('WgerHttpException', () { + test('parses valid JSON response', () { + // Arrange + final resp = http.Response( + '{"foo":"bar"}', + 400, + headers: {HttpHeaders.contentTypeHeader: 'application/json'}, + ); + + // Act + final ex = WgerHttpException(resp); + + // Assert + expect(ex.type, ErrorType.json); + expect(ex.errors['foo'], 'bar'); + expect(ex.toString(), contains('WgerHttpException')); + }); + + test('falls back on malformed JSON', () { + // Arrange + const body = '{"foo":'; + final resp = http.Response( + body, + 500, + headers: {HttpHeaders.contentTypeHeader: 'application/json'}, + ); + + // Act + final ex = WgerHttpException(resp); + + // Assert + expect(ex.type, ErrorType.json); + expect(ex.errors['unknown_error'], body); + }); + + test('detects HTML response from headers', () { + // Arrange + const body = 'Error'; + final resp = http.Response( + body, + 500, + headers: {HttpHeaders.contentTypeHeader: 'text/html; charset=utf-8'}, + ); + + // Act + final ex = WgerHttpException(resp); + + // Assert + expect(ex.type, ErrorType.html); + expect(ex.htmlError, body); + }); + + test('detects HTML response from content', () { + // Arrange + const body = 'Error'; + final resp = http.Response( + body, + 500, + headers: {HttpHeaders.contentTypeHeader: 'text/foo; charset=utf-8'}, + ); + + // Act + final ex = WgerHttpException(resp); + + // Assert + expect(ex.type, ErrorType.html); + expect(ex.htmlError, body); + }); + + test('fromMap sets errors and type', () { + // Arrange + final map = {'field': 'value'}; + + // Act + final ex = WgerHttpException.fromMap(map); + + // Assert + expect(ex.type, ErrorType.json); + expect(ex.errors, map); + }); + }); +} diff --git a/test/core/settings_test.mocks.dart b/test/core/settings_test.mocks.dart index db2394bd..4e060322 100644 --- a/test/core/settings_test.mocks.dart +++ b/test/core/settings_test.mocks.dart @@ -1100,9 +1100,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { as Uri); @override - _i18.Future fetch(Uri? uri) => + _i18.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i18.Future.value(), ) as _i18.Future); diff --git a/test/core/validators_test.mocks.dart b/test/core/validators_test.mocks.dart index f78fcd69..9b069055 100644 --- a/test/core/validators_test.mocks.dart +++ b/test/core/validators_test.mocks.dart @@ -20,6 +20,7 @@ import 'package:wger/l10n/generated/app_localizations.dart' as _i2; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member /// A class which mocks [AppLocalizations]. /// @@ -919,6 +920,39 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String get impressionGood => + (super.noSuchMethod( + Invocation.getter(#impressionGood), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#impressionGood), + ), + ) + as String); + + @override + String get impressionNeutral => + (super.noSuchMethod( + Invocation.getter(#impressionNeutral), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#impressionNeutral), + ), + ) + as String); + + @override + String get impressionBad => + (super.noSuchMethod( + Invocation.getter(#impressionBad), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#impressionBad), + ), + ) + as String); + @override String get impression => (super.noSuchMethod( @@ -1095,6 +1129,105 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String get gymModeTimerType => + (super.noSuchMethod( + Invocation.getter(#gymModeTimerType), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#gymModeTimerType), + ), + ) + as String); + + @override + String get gymModeTimerTypeHelText => + (super.noSuchMethod( + Invocation.getter(#gymModeTimerTypeHelText), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#gymModeTimerTypeHelText), + ), + ) + as String); + + @override + String get countdown => + (super.noSuchMethod( + Invocation.getter(#countdown), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#countdown), + ), + ) + as String); + + @override + String get stopwatch => + (super.noSuchMethod( + Invocation.getter(#stopwatch), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#stopwatch), + ), + ) + as String); + + @override + String get gymModeDefaultCountdownTime => + (super.noSuchMethod( + Invocation.getter(#gymModeDefaultCountdownTime), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#gymModeDefaultCountdownTime), + ), + ) + as String); + + @override + String get gymModeNotifyOnCountdownFinish => + (super.noSuchMethod( + Invocation.getter(#gymModeNotifyOnCountdownFinish), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#gymModeNotifyOnCountdownFinish), + ), + ) + as String); + + @override + String get duration => + (super.noSuchMethod( + Invocation.getter(#duration), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#duration), + ), + ) + as String); + + @override + String get volume => + (super.noSuchMethod( + Invocation.getter(#volume), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#volume), + ), + ) + as String); + + @override + String get workoutCompleted => + (super.noSuchMethod( + Invocation.getter(#workoutCompleted), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#workoutCompleted), + ), + ) + as String); + @override String get plateCalculator => (super.noSuchMethod( @@ -3677,6 +3810,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String durationHoursMinutes(int? hours, int? minutes) => + (super.noSuchMethod( + Invocation.method(#durationHoursMinutes, [hours, minutes]), + returnValue: _i3.dummyValue( + this, + Invocation.method(#durationHoursMinutes, [hours, minutes]), + ), + ) + as String); + @override String chartAllTimeTitle(String? name) => (super.noSuchMethod( @@ -3765,6 +3909,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String formMinMaxValues(int? min, int? max) => + (super.noSuchMethod( + Invocation.method(#formMinMaxValues, [min, max]), + returnValue: _i3.dummyValue( + this, + Invocation.method(#formMinMaxValues, [min, max]), + ), + ) + as String); + @override String enterMinCharacters(String? min) => (super.noSuchMethod( diff --git a/test/exercises/contribute_exercise_image_test.mocks.dart b/test/exercises/contribute_exercise_image_test.mocks.dart index c3685ed3..c20d1d75 100644 --- a/test/exercises/contribute_exercise_image_test.mocks.dart +++ b/test/exercises/contribute_exercise_image_test.mocks.dart @@ -402,9 +402,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { as Uri); @override - _i14.Future fetch(Uri? uri) => + _i14.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i14.Future.value(), ) as _i14.Future); diff --git a/test/exercises/contribute_exercise_test.dart b/test/exercises/contribute_exercise_test.dart index a909efc0..df1c3b4b 100644 --- a/test/exercises/contribute_exercise_test.dart +++ b/test/exercises/contribute_exercise_test.dart @@ -21,7 +21,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/providers/add_exercise.dart'; import 'package:wger/providers/exercises.dart'; @@ -422,7 +422,7 @@ void main() { testWidgets('Failed submission displays error message', (WidgetTester tester) async { // Setup: Create verified user and mock failed submission setupFullVerifiedUserContext(); - final httpException = WgerHttpException({ + final httpException = WgerHttpException.fromMap({ 'name': ['This field is required'], }); when(mockAddExerciseProvider.postExerciseToServer()).thenThrow(httpException); diff --git a/test/exercises/exercise_provider_db_test.dart b/test/exercises/exercise_provider_db_test.dart index 3862baa6..2d0c1f8b 100644 --- a/test/exercises/exercise_provider_db_test.dart +++ b/test/exercises/exercise_provider_db_test.dart @@ -1,3 +1,21 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import 'dart:convert'; import 'package:drift/native.dart'; @@ -100,19 +118,25 @@ void main() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); // Mock categories - when(mockBaseProvider.makeUrl(categoryUrl)).thenReturn(tCategoryEntriesUri); + when( + mockBaseProvider.makeUrl(categoryUrl, query: anyNamed('query')), + ).thenReturn(tCategoryEntriesUri); when( mockBaseProvider.fetchPaginated(tCategoryEntriesUri), ).thenAnswer((_) => Future.value(tCategoryMap['results'])); // Mock muscles - when(mockBaseProvider.makeUrl(muscleUrl)).thenReturn(tMuscleEntriesUri); + when( + mockBaseProvider.makeUrl(muscleUrl, query: anyNamed('query')), + ).thenReturn(tMuscleEntriesUri); when( mockBaseProvider.fetchPaginated(tMuscleEntriesUri), ).thenAnswer((_) => Future.value(tMuscleMap['results'])); // Mock equipment - when(mockBaseProvider.makeUrl(equipmentUrl)).thenReturn(tEquipmentEntriesUri); + when( + mockBaseProvider.makeUrl(equipmentUrl, query: anyNamed('query')), + ).thenReturn(tEquipmentEntriesUri); when( mockBaseProvider.fetchPaginated(tEquipmentEntriesUri), ).thenAnswer((_) => Future.value(tEquipmentMap['results'])); diff --git a/test/exercises/exercise_provider_test.dart b/test/exercises/exercise_provider_test.dart index 82a9d861..d597759c 100644 --- a/test/exercises/exercise_provider_test.dart +++ b/test/exercises/exercise_provider_test.dart @@ -1,3 +1,21 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import 'dart:convert'; import 'package:drift/drift.dart'; @@ -7,8 +25,8 @@ import 'package:mockito/mockito.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/database/exercises/exercise_database.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/models/exercises/category.dart'; @@ -105,19 +123,25 @@ void main() { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; // Mock categories - when(mockBaseProvider.makeUrl(categoryUrl)).thenReturn(tCategoryEntriesUri); + when( + mockBaseProvider.makeUrl(categoryUrl, query: anyNamed('query')), + ).thenReturn(tCategoryEntriesUri); when( mockBaseProvider.fetchPaginated(tCategoryEntriesUri), ).thenAnswer((_) => Future.value(tCategoryMap['results'])); // Mock muscles - when(mockBaseProvider.makeUrl(muscleUrl)).thenReturn(tMuscleEntriesUri); + when( + mockBaseProvider.makeUrl(muscleUrl, query: anyNamed('query')), + ).thenReturn(tMuscleEntriesUri); when( mockBaseProvider.fetchPaginated(tMuscleEntriesUri), ).thenAnswer((_) => Future.value(tMuscleMap['results'])); // Mock equipment - when(mockBaseProvider.makeUrl(equipmentUrl)).thenReturn(tEquipmentEntriesUri); + when( + mockBaseProvider.makeUrl(equipmentUrl, query: anyNamed('query')), + ).thenReturn(tEquipmentEntriesUri); when( mockBaseProvider.fetchPaginated(tEquipmentEntriesUri), ).thenAnswer((_) => Future.value(tEquipmentMap['results'])); diff --git a/test/gallery/gallery_form_test.mocks.dart b/test/gallery/gallery_form_test.mocks.dart index e14d7838..ad71121f 100644 --- a/test/gallery/gallery_form_test.mocks.dart +++ b/test/gallery/gallery_form_test.mocks.dart @@ -175,9 +175,17 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { as Uri); @override - _i6.Future fetch(Uri? uri) => + _i6.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i6.Future.value(), ) as _i6.Future); diff --git a/test/gallery/gallery_screen_test.mocks.dart b/test/gallery/gallery_screen_test.mocks.dart index d1ab1318..7ea2e32f 100644 --- a/test/gallery/gallery_screen_test.mocks.dart +++ b/test/gallery/gallery_screen_test.mocks.dart @@ -175,9 +175,17 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { as Uri); @override - _i6.Future fetch(Uri? uri) => + _i6.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i6.Future.value(), ) as _i6.Future); diff --git a/test/utils/errors_test.dart b/test/helpers/errors_test.dart similarity index 100% rename from test/utils/errors_test.dart rename to test/helpers/errors_test.dart diff --git a/test/measurements/measurement_category_test.dart b/test/measurements/measurement_category_test.dart index 21009d0e..3e16394d 100644 --- a/test/measurements/measurement_category_test.dart +++ b/test/measurements/measurement_category_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/models/measurements/measurement_entry.dart'; diff --git a/test/measurements/measurement_provider_test.dart b/test/measurements/measurement_provider_test.dart index 799ac261..675f6c1b 100644 --- a/test/measurements/measurement_provider_test.dart +++ b/test/measurements/measurement_provider_test.dart @@ -4,8 +4,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:wger/exceptions/http_exception.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/models/measurements/measurement_entry.dart'; @@ -271,7 +271,7 @@ void main() { 'should re-add the "removed" MeasurementCategory and relay the exception on WgerHttpException', () { // arrange - when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException('{}')); + when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException.fromMap({})); // act & assert expect( @@ -330,7 +330,7 @@ void main() { test('should keep categories list as is on WgerHttpException', () { // arrange - when(mockWgerBaseProvider.patch(any, any)).thenThrow(WgerHttpException('{}')); + when(mockWgerBaseProvider.patch(any, any)).thenThrow(WgerHttpException.fromMap({})); // act & assert expect( @@ -550,7 +550,7 @@ void main() { ), const MeasurementCategory(id: 2, name: 'Biceps', unit: 'cm'), ]; - when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException('{}')); + when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException.fromMap({})); // act & assert expect( diff --git a/test/measurements/measurement_provider_test.mocks.dart b/test/measurements/measurement_provider_test.mocks.dart index ef362a87..7c766730 100644 --- a/test/measurements/measurement_provider_test.mocks.dart +++ b/test/measurements/measurement_provider_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/nutrition/nutritional_plan_screen_test.mocks.dart b/test/nutrition/nutritional_plan_screen_test.mocks.dart index 2fe3c4ac..9b7d92ee 100644 --- a/test/nutrition/nutritional_plan_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plan_screen_test.mocks.dart @@ -122,9 +122,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/nutrition/nutritional_plans_screen_test.mocks.dart b/test/nutrition/nutritional_plans_screen_test.mocks.dart index c702d401..1d9f0fb0 100644 --- a/test/nutrition/nutritional_plans_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plans_screen_test.mocks.dart @@ -357,9 +357,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i8.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/providers/base_provider.dart b/test/providers/base_provider.dart new file mode 100644 index 00000000..203973c3 --- /dev/null +++ b/test/providers/base_provider.dart @@ -0,0 +1,135 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; +import 'package:wger/providers/base_provider.dart'; + +import '../utils.dart'; +import 'base_provider.mocks.dart'; + +@GenerateMocks([Client]) +void main() { + final Uri testUri = Uri(scheme: 'https', host: 'localhost', path: 'api/v2/test/'); + + test('Retry on SocketException then succeeds', () async { + // Arrange + final mockClient = MockClient(); + var callCount = 0; + when(mockClient.get(testUri, headers: anyNamed('headers'))).thenAnswer((_) { + if (callCount == 0) { + callCount++; + return Future.error(const SocketException('conn fail')); + } + return Future.value(Response('{"ok": true}', 200)); + }); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + final result = await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)); + + // Assert + expect(result, isA()); + expect(result['ok'], isTrue); + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(2); + }); + + test('Retry on 5xx then succeeds', () async { + // Arrange + final mockClient = MockClient(); + var callCount = 0; + when(mockClient.get(testUri, headers: anyNamed('headers'))).thenAnswer((_) { + if (callCount == 0) { + callCount++; + return Future.value(Response('{"msg":"error"}', 502)); + } + return Future.value(Response('{"ok": true}', 200)); + }); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + final result = await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)); + + // Assert + expect(result, isA()); + expect(result['ok'], isTrue); + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(2); + }); + + test('Do not retry on 4xx client error', () async { + // Arrange + final mockClient = MockClient(); + when( + mockClient.get(testUri, headers: anyNamed('headers')), + ).thenAnswer((_) => Future.value(Response('{"error":"bad"}', 400))); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + + // Assert + await expectLater( + provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)), + throwsA(isA()), + ); + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(1); + }); + + test('Exceed max retries and rethrow after retries', () async { + // Arrange + final mockClient = MockClient(); + when( + mockClient.get(testUri, headers: anyNamed('headers')), + ).thenAnswer((_) => Future.error(ClientException('conn fail'))); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + dynamic caught; + try { + await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)); + } catch (e) { + caught = e; + } + + // Assert + expect(caught, isA()); + // initial try + 3 retries = 4 calls + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(4); + }); + + test('Request succeeds without retries', () async { + // Arrange + final mockClient = MockClient(); + when( + mockClient.get(testUri, headers: anyNamed('headers')), + ).thenAnswer((_) => Future.value(Response('{"ok": true}', 200))); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + final result = await provider.fetch(testUri); + + // Assert + expect(result, isA()); + expect(result['ok'], isTrue); + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(1); + }); +} diff --git a/test/providers/base_provider.mocks.dart b/test/providers/base_provider.mocks.dart new file mode 100644 index 00000000..5111102b --- /dev/null +++ b/test/providers/base_provider.mocks.dart @@ -0,0 +1,218 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in wger/test/providers/base_provider.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: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member + +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/providers/plate_calculator_test.mocks.dart b/test/providers/plate_calculator_test.mocks.dart index bcba37ab..9b3fed8e 100644 --- a/test/providers/plate_calculator_test.mocks.dart +++ b/test/providers/plate_calculator_test.mocks.dart @@ -21,6 +21,7 @@ import 'package:shared_preferences/src/shared_preferences_async.dart' as _i2; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member /// A class which mocks [SharedPreferencesAsync]. /// diff --git a/test/routine/forms/session_form_test.dart b/test/routine/forms/session_form_test.dart index 53302ea9..53febec2 100644 --- a/test/routine/forms/session_form_test.dart +++ b/test/routine/forms/session_form_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/session.dart'; import 'package:wger/providers/routines.dart'; diff --git a/test/routine/gym_mode/gym_mode_test.dart b/test/routine/gym_mode/gym_mode_test.dart index 38a3e296..3f61e88b 100644 --- a/test/routine/gym_mode/gym_mode_test.dart +++ b/test/routine/gym_mode/gym_mode_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020, 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -109,12 +109,10 @@ void main() { await withClock(Clock.fixed(DateTime(2025, 3, 29, 14, 33)), () async { await tester.pumpWidget(renderGymMode()); - await tester.pumpAndSettle(); await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); - //await tester.ensureVisible(find.byKey(Key(key as String))); + // // Start page // @@ -306,6 +304,7 @@ void main() { expect(find.byIcon(Icons.chevron_right), findsNothing); }); }, + tags: ['golden'], semanticsEnabled: false, ); } diff --git a/test/routine/gym_mode/gym_mode_test.mocks.dart b/test/routine/gym_mode/gym_mode_test.mocks.dart index 6b3c882c..9541140d 100644 --- a/test/routine/gym_mode/gym_mode_test.mocks.dart +++ b/test/routine/gym_mode/gym_mode_test.mocks.dart @@ -201,9 +201,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i20.Future fetch(Uri? uri) => + _i20.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i20.Future.value(), ) as _i20.Future); diff --git a/test/routine/gym_mode/session_page_test.dart b/test/routine/gym_mode/session_page_test.dart index 63bc210c..60382d66 100644 --- a/test/routine/gym_mode/session_page_test.dart +++ b/test/routine/gym_mode/session_page_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020, wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -56,6 +56,9 @@ void main() { when(mockRoutinesProvider.editSession(any)).thenAnswer( (_) => Future.value(testRoutine.sessions[0].session), ); + when(mockRoutinesProvider.fetchAndSetRoutineFull(any)).thenAnswer( + (_) => Future.value(testRoutine), + ); }); Widget renderSessionPage({locale = 'en'}) { @@ -85,6 +88,9 @@ void main() { testWidgets('Test that data from session is loaded', (WidgetTester tester) async { withClock(Clock.fixed(DateTime(2021, 5, 1)), () async { await tester.pumpWidget(renderSessionPage()); + await tester.pumpAndSettle(); + + debugDumpApp(); expect(find.text('10:00'), findsOneWidget); expect(find.text('12:34'), findsOneWidget); expect(find.text('This is a note'), findsOneWidget); @@ -102,6 +108,7 @@ void main() { withClock(Clock.fixed(DateTime(2021, 5, 1)), () async { await tester.pumpWidget(renderSessionPage()); + await tester.pumpAndSettle(); final startTimeField = find.byKey(const ValueKey('time-start')); expect(startTimeField, findsOneWidget); @@ -123,6 +130,7 @@ void main() { // Act await tester.pumpWidget(renderSessionPage()); + await tester.pumpAndSettle(); // Assert expect(find.text('13:35'), findsOneWidget); @@ -134,11 +142,11 @@ void main() { testWidgets('Test that correct data is send to server', (WidgetTester tester) async { withClock(Clock.fixed(DateTime(2021, 5, 1)), () async { await tester.pumpWidget(renderSessionPage()); + await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('save-button'))); final captured = verify(mockRoutinesProvider.editSession(captureAny)).captured.single as WorkoutSession; - print(captured); expect(captured.id, 1); expect(captured.impression, 3); expect(captured.notes, equals('This is a note')); diff --git a/test/routine/routine_form_test.dart b/test/routine/routine_form_test.dart index 8f287f09..c40c7426 100644 --- a/test/routine/routine_form_test.dart +++ b/test/routine/routine_form_test.dart @@ -21,7 +21,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/routine.dart'; diff --git a/test/routine/routine_screen_test.mocks.dart b/test/routine/routine_screen_test.mocks.dart index 99fe1643..7c623651 100644 --- a/test/routine/routine_screen_test.mocks.dart +++ b/test/routine/routine_screen_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/routine/routines_provider_test.mocks.dart b/test/routine/routines_provider_test.mocks.dart index 06326506..3fc8c4ee 100644 --- a/test/routine/routines_provider_test.mocks.dart +++ b/test/routine/routines_provider_test.mocks.dart @@ -151,9 +151,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i11.Future fetch(Uri? uri) => + _i11.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i11.Future.value(), ) as _i11.Future); diff --git a/test/routine/routines_screen_test.mocks.dart b/test/routine/routines_screen_test.mocks.dart index 2607f048..95d3ca97 100644 --- a/test/routine/routines_screen_test.mocks.dart +++ b/test/routine/routines_screen_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/user/provider_test.mocks.dart b/test/user/provider_test.mocks.dart index c0b363ab..4e7436d0 100644 --- a/test/user/provider_test.mocks.dart +++ b/test/user/provider_test.mocks.dart @@ -23,6 +23,7 @@ import 'package:wger/providers/base_provider.dart' as _i4; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member class _FakeAuthProvider_0 extends _i1.SmartFake implements _i2.AuthProvider { _FakeAuthProvider_0(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); @@ -65,14 +66,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as _i3.Client); @override - set auth(_i2.AuthProvider? _auth) => super.noSuchMethod( - Invocation.setter(#auth, _auth), + set auth(_i2.AuthProvider? value) => super.noSuchMethod( + Invocation.setter(#auth, value), returnValueForMissingStub: null, ); @override - set client(_i3.Client? _client) => super.noSuchMethod( - Invocation.setter(#client, _client), + set client(_i3.Client? value) => super.noSuchMethod( + Invocation.setter(#client, value), returnValueForMissingStub: null, ); @@ -111,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/weight/weight_provider_test.mocks.dart b/test/weight/weight_provider_test.mocks.dart index 4cd18bb5..be3d3857 100644 --- a/test/weight/weight_provider_test.mocks.dart +++ b/test/weight/weight_provider_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/widgets/routines/gym_mode/log_page_test.dart b/test/widgets/routines/gym_mode/log_page_test.dart new file mode 100644 index 00000000..b9ba0f8d --- /dev/null +++ b/test/widgets/routines/gym_mode/log_page_test.dart @@ -0,0 +1,241 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 - 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart' as provider; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/models/exercises/exercise.dart'; +import 'package:wger/models/workouts/day_data.dart'; +import 'package:wger/models/workouts/log.dart'; +import 'package:wger/models/workouts/set_config_data.dart'; +import 'package:wger/models/workouts/slot_data.dart'; +import 'package:wger/providers/exercises.dart'; +import 'package:wger/providers/gym_state.dart'; +import 'package:wger/providers/routines.dart'; +import 'package:wger/widgets/routines/gym_mode/log_page.dart'; + +import '../../../../test_data/exercises.dart'; +import '../../../../test_data/routines.dart' as testdata; +import 'log_page_test.mocks.dart'; + +@GenerateMocks([ExercisesProvider, RoutinesProvider]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('LogPage tests', () { + late List testExercises; + late ProviderContainer container; + + setUp(() { + SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); + testExercises = getTestExercises(); + container = ProviderContainer.test(); + }); + + Future pumpLogPage(WidgetTester tester, {RoutinesProvider? routinesProvider}) async { + final providerValue = routinesProvider ?? MockRoutinesProvider(); + + await tester.pumpWidget( + UncontrolledProviderScope( + container: container, + child: provider.ChangeNotifierProvider.value( + value: providerValue, + child: MaterialApp( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + // Provide a PageView so the PageController used by LogPage is attached + body: Builder( + builder: (context) { + final controller = PageController(); + return PageView( + controller: controller, + children: [LogPage(controller)], + ); + }, + ), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + } + + testWidgets('handles null values', (tester) async { + // Arrange + final notifier = container.read(gymStateProvider.notifier); + final routine = testdata.getTestRoutine(); + routine.dayDataGym = [ + DayData( + iteration: 1, + date: DateTime(2024, 11, 01), + label: '', + day: routine.dayDataGym.first.day, + slots: [ + SlotData( + isSuperset: false, + exerciseIds: [testExercises[0].id!], + setConfigs: [ + SetConfigData( + exerciseId: testExercises[0].id!, + exercise: testExercises[0], + slotEntryId: 1, + nrOfSets: 1, + repetitions: null, + repetitionsUnit: null, + weight: null, + weightUnit: null, + restTime: 120, + rir: 1.5, + rpe: 8, + textRepr: '3x100kg', + ), + ], + ), + ], + ), + ]; + notifier.state = notifier.state.copyWith( + dayId: routine.days.first.id, + routine: routine, + iteration: 1, + currentPage: 2, + ); + + // Act + notifier.calculatePages(); + notifier.setCurrentPage(2); + + // Assert + expect(notifier.state.getSlotEntryPageByIndex()!.type, SlotPageType.log); + await pumpLogPage(tester); + expect(find.byType(LogPage), findsOneWidget); + }); + + testWidgets('renders without crashing for default slotEntryPage', (tester) async { + final notifier = container.read(gymStateProvider.notifier); + final routine = testdata.getTestRoutine(); + notifier.state = notifier.state.copyWith( + dayId: routine.days.first.id, + routine: routine, + iteration: 1, + ); + notifier.calculatePages(); + await pumpLogPage(tester); + + expect(find.byType(LogPage), findsOneWidget); + }); + + testWidgets('copy from past log updates form fields and shows SnackBar', (tester) async { + // Arrange + final notifier = container.read(gymStateProvider.notifier); + final routine = testdata.getTestRoutine(); + notifier.state = notifier.state.copyWith( + dayId: routine.days.first.id, + routine: routine, + iteration: 1, + ); + notifier.calculatePages(); + notifier.setCurrentPage(2); + + // Act + // Log page is at index 2 + notifier.state = notifier.state.copyWith(currentPage: 2); + expect(notifier.state.getSlotEntryPageByIndex()!.type, SlotPageType.log); + await pumpLogPage(tester); + + // Assert + final pastLogTile = find.byKey(const ValueKey('past-log-1')); + expect(pastLogTile, findsOneWidget); + await tester.tap(pastLogTile); + await tester.pumpAndSettle(); + + final editableFields = find.byType(EditableText); + expect(editableFields, findsWidgets); + + // Get controller texts + final repControllerText = tester.widget(editableFields.at(0)).controller.text; + final weightControllerText = tester + .widget(editableFields.at(1)) + .controller + .text; + + expect(repControllerText, contains('10')); + expect(weightControllerText, contains('10')); + expect(find.byType(SnackBar), findsOneWidget); + }); + + testWidgets('save button calls addLog on RoutinesProvider', (tester) async { + // Arrange + final notifier = container.read(gymStateProvider.notifier); + final routine = testdata.getTestRoutine(); + notifier.state = notifier.state.copyWith( + dayId: routine.days.first.id, + routine: routine, + iteration: 1, + ); + notifier.calculatePages(); + notifier.setCurrentPage(2); + notifier.state = notifier.state.copyWith(currentPage: 2); + final mockRoutines = MockRoutinesProvider(); + + // Act + await pumpLogPage(tester, routinesProvider: mockRoutines); + + final editableFields = find.byType(EditableText); + expect(editableFields, findsWidgets); + + await tester.enterText(editableFields.at(0), '12'); // Reps + await tester.enterText(editableFields.at(1), '34'); // Weight + await tester.pumpAndSettle(); + + Log? capturedLog; + when(mockRoutines.addLog(any)).thenAnswer((invocation) async { + capturedLog = invocation.positionalArguments[0] as Log; + capturedLog!.id = 42; + return capturedLog!; + }); + + final saveButton = find.byKey(const ValueKey('save-log-button')); + expect(saveButton, findsOneWidget); + + await tester.tap(saveButton); + await tester.pumpAndSettle(); + + // Assert + verify(mockRoutines.addLog(any)).called(1); + expect(capturedLog, isNotNull); + expect(capturedLog!.repetitions, equals(12)); + expect(capturedLog!.weight, equals(34)); + + final currentSlotPage = notifier.state.getSlotEntryPageByIndex()!; + expect(capturedLog!.slotEntryId, equals(currentSlotPage.setConfigData!.slotEntryId)); + expect(capturedLog!.routineId, equals(notifier.state.routine.id)); + expect(capturedLog!.iteration, equals(notifier.state.iteration)); + }); + }); +} diff --git a/test/widgets/routines/gym_mode/log_page_test.mocks.dart b/test/widgets/routines/gym_mode/log_page_test.mocks.dart new file mode 100644 index 00000000..e9105c48 --- /dev/null +++ b/test/widgets/routines/gym_mode/log_page_test.mocks.dart @@ -0,0 +1,1051 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in wger/test/widgets/routines/gym_mode/log_page_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i19; +import 'dart:ui' as _i20; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i23; +import 'package:wger/database/exercises/exercise_database.dart' as _i3; +import 'package:wger/models/exercises/category.dart' as _i5; +import 'package:wger/models/exercises/equipment.dart' as _i6; +import 'package:wger/models/exercises/exercise.dart' as _i4; +import 'package:wger/models/exercises/language.dart' as _i8; +import 'package:wger/models/exercises/muscle.dart' as _i7; +import 'package:wger/models/workouts/base_config.dart' as _i15; +import 'package:wger/models/workouts/day.dart' as _i12; +import 'package:wger/models/workouts/day_data.dart' as _i22; +import 'package:wger/models/workouts/log.dart' as _i17; +import 'package:wger/models/workouts/repetition_unit.dart' as _i10; +import 'package:wger/models/workouts/routine.dart' as _i11; +import 'package:wger/models/workouts/session.dart' as _i16; +import 'package:wger/models/workouts/slot.dart' as _i13; +import 'package:wger/models/workouts/slot_entry.dart' as _i14; +import 'package:wger/models/workouts/weight_unit.dart' as _i9; +import 'package:wger/providers/base_provider.dart' as _i2; +import 'package:wger/providers/exercises.dart' as _i18; +import 'package:wger/providers/routines.dart' as _i21; + +// 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: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member + +class _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider { + _FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeExerciseDatabase_1 extends _i1.SmartFake implements _i3.ExerciseDatabase { + _FakeExerciseDatabase_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeExercise_2 extends _i1.SmartFake implements _i4.Exercise { + _FakeExercise_2(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeExerciseCategory_3 extends _i1.SmartFake implements _i5.ExerciseCategory { + _FakeExerciseCategory_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeEquipment_4 extends _i1.SmartFake implements _i6.Equipment { + _FakeEquipment_4(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeMuscle_5 extends _i1.SmartFake implements _i7.Muscle { + _FakeMuscle_5(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeLanguage_6 extends _i1.SmartFake implements _i8.Language { + _FakeLanguage_6(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeWeightUnit_7 extends _i1.SmartFake implements _i9.WeightUnit { + _FakeWeightUnit_7(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeRepetitionUnit_8 extends _i1.SmartFake implements _i10.RepetitionUnit { + _FakeRepetitionUnit_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeRoutine_9 extends _i1.SmartFake implements _i11.Routine { + _FakeRoutine_9(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeDay_10 extends _i1.SmartFake implements _i12.Day { + _FakeDay_10(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeSlot_11 extends _i1.SmartFake implements _i13.Slot { + _FakeSlot_11(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeSlotEntry_12 extends _i1.SmartFake implements _i14.SlotEntry { + _FakeSlotEntry_12(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeBaseConfig_13 extends _i1.SmartFake implements _i15.BaseConfig { + _FakeBaseConfig_13(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeWorkoutSession_14 extends _i1.SmartFake implements _i16.WorkoutSession { + _FakeWorkoutSession_14(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeLog_15 extends _i1.SmartFake implements _i17.Log { + _FakeLog_15(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +/// A class which mocks [ExercisesProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockExercisesProvider extends _i1.Mock implements _i18.ExercisesProvider { + MockExercisesProvider() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WgerBaseProvider get baseProvider => + (super.noSuchMethod( + Invocation.getter(#baseProvider), + returnValue: _FakeWgerBaseProvider_0( + this, + Invocation.getter(#baseProvider), + ), + ) + as _i2.WgerBaseProvider); + + @override + _i3.ExerciseDatabase get database => + (super.noSuchMethod( + Invocation.getter(#database), + returnValue: _FakeExerciseDatabase_1( + this, + Invocation.getter(#database), + ), + ) + as _i3.ExerciseDatabase); + + @override + List<_i4.Exercise> get exercises => + (super.noSuchMethod( + Invocation.getter(#exercises), + returnValue: <_i4.Exercise>[], + ) + as List<_i4.Exercise>); + + @override + List<_i4.Exercise> get filteredExercises => + (super.noSuchMethod( + Invocation.getter(#filteredExercises), + returnValue: <_i4.Exercise>[], + ) + as List<_i4.Exercise>); + + @override + Map> get exerciseByVariation => + (super.noSuchMethod( + Invocation.getter(#exerciseByVariation), + returnValue: >{}, + ) + as Map>); + + @override + List<_i5.ExerciseCategory> get categories => + (super.noSuchMethod( + Invocation.getter(#categories), + returnValue: <_i5.ExerciseCategory>[], + ) + as List<_i5.ExerciseCategory>); + + @override + List<_i7.Muscle> get muscles => + (super.noSuchMethod( + Invocation.getter(#muscles), + returnValue: <_i7.Muscle>[], + ) + as List<_i7.Muscle>); + + @override + List<_i6.Equipment> get equipment => + (super.noSuchMethod( + Invocation.getter(#equipment), + returnValue: <_i6.Equipment>[], + ) + as List<_i6.Equipment>); + + @override + List<_i8.Language> get languages => + (super.noSuchMethod( + Invocation.getter(#languages), + returnValue: <_i8.Language>[], + ) + as List<_i8.Language>); + + @override + set database(_i3.ExerciseDatabase? value) => super.noSuchMethod( + Invocation.setter(#database, value), + returnValueForMissingStub: null, + ); + + @override + set exercises(List<_i4.Exercise>? value) => super.noSuchMethod( + Invocation.setter(#exercises, value), + returnValueForMissingStub: null, + ); + + @override + set filteredExercises(List<_i4.Exercise>? newFilteredExercises) => super.noSuchMethod( + Invocation.setter(#filteredExercises, newFilteredExercises), + returnValueForMissingStub: null, + ); + + @override + set languages(List<_i8.Language>? languages) => super.noSuchMethod( + Invocation.setter(#languages, languages), + returnValueForMissingStub: null, + ); + + @override + bool get hasListeners => + (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) as bool); + + @override + _i19.Future setFilters(_i18.Filters? newFilters) => + (super.noSuchMethod( + Invocation.method(#setFilters, [newFilters]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + void initFilters() => super.noSuchMethod( + Invocation.method(#initFilters, []), + returnValueForMissingStub: null, + ); + + @override + _i19.Future findByFilters() => + (super.noSuchMethod( + Invocation.method(#findByFilters, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + void clear() => super.noSuchMethod( + Invocation.method(#clear, []), + returnValueForMissingStub: null, + ); + + @override + _i4.Exercise findExerciseById(int? id) => + (super.noSuchMethod( + Invocation.method(#findExerciseById, [id]), + returnValue: _FakeExercise_2( + this, + Invocation.method(#findExerciseById, [id]), + ), + ) + as _i4.Exercise); + + @override + List<_i4.Exercise> findExercisesByVariationId( + int? variationId, { + int? exerciseIdToExclude, + }) => + (super.noSuchMethod( + Invocation.method( + #findExercisesByVariationId, + [variationId], + {#exerciseIdToExclude: exerciseIdToExclude}, + ), + returnValue: <_i4.Exercise>[], + ) + as List<_i4.Exercise>); + + @override + _i5.ExerciseCategory findCategoryById(int? id) => + (super.noSuchMethod( + Invocation.method(#findCategoryById, [id]), + returnValue: _FakeExerciseCategory_3( + this, + Invocation.method(#findCategoryById, [id]), + ), + ) + as _i5.ExerciseCategory); + + @override + _i6.Equipment findEquipmentById(int? id) => + (super.noSuchMethod( + Invocation.method(#findEquipmentById, [id]), + returnValue: _FakeEquipment_4( + this, + Invocation.method(#findEquipmentById, [id]), + ), + ) + as _i6.Equipment); + + @override + _i7.Muscle findMuscleById(int? id) => + (super.noSuchMethod( + Invocation.method(#findMuscleById, [id]), + returnValue: _FakeMuscle_5( + this, + Invocation.method(#findMuscleById, [id]), + ), + ) + as _i7.Muscle); + + @override + _i8.Language findLanguageById(int? id) => + (super.noSuchMethod( + Invocation.method(#findLanguageById, [id]), + returnValue: _FakeLanguage_6( + this, + Invocation.method(#findLanguageById, [id]), + ), + ) + as _i8.Language); + + @override + _i19.Future fetchAndSetCategoriesFromApi() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetCategoriesFromApi, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetMusclesFromApi() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetMusclesFromApi, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetEquipmentsFromApi() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetEquipmentsFromApi, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetLanguagesFromApi() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetLanguagesFromApi, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetAllExercises() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetAllExercises, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future<_i4.Exercise?> fetchAndSetExercise(int? exerciseId) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetExercise, [exerciseId]), + returnValue: _i19.Future<_i4.Exercise?>.value(), + ) + as _i19.Future<_i4.Exercise?>); + + @override + _i19.Future<_i4.Exercise> handleUpdateExerciseFromApi( + _i3.ExerciseDatabase? database, + int? exerciseId, + ) => + (super.noSuchMethod( + Invocation.method(#handleUpdateExerciseFromApi, [ + database, + exerciseId, + ]), + returnValue: _i19.Future<_i4.Exercise>.value( + _FakeExercise_2( + this, + Invocation.method(#handleUpdateExerciseFromApi, [ + database, + exerciseId, + ]), + ), + ), + ) + as _i19.Future<_i4.Exercise>); + + @override + _i19.Future initCacheTimesLocalPrefs({dynamic forceInit = false}) => + (super.noSuchMethod( + Invocation.method(#initCacheTimesLocalPrefs, [], { + #forceInit: forceInit, + }), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future clearAllCachesAndPrefs() => + (super.noSuchMethod( + Invocation.method(#clearAllCachesAndPrefs, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetInitialData() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetInitialData, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future setExercisesFromDatabase( + _i3.ExerciseDatabase? database, { + bool? forceDeleteCache = false, + }) => + (super.noSuchMethod( + Invocation.method( + #setExercisesFromDatabase, + [database], + {#forceDeleteCache: forceDeleteCache}, + ), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future updateExerciseCache(_i3.ExerciseDatabase? database) => + (super.noSuchMethod( + Invocation.method(#updateExerciseCache, [database]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetMuscles(_i3.ExerciseDatabase? database) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetMuscles, [database]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetCategories(_i3.ExerciseDatabase? database) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetCategories, [database]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetLanguages(_i3.ExerciseDatabase? database) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetLanguages, [database]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetEquipments(_i3.ExerciseDatabase? database) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetEquipments, [database]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future> searchExercise( + String? name, { + String? languageCode = 'en', + bool? searchEnglish = false, + }) => + (super.noSuchMethod( + Invocation.method( + #searchExercise, + [name], + {#languageCode: languageCode, #searchEnglish: searchEnglish}, + ), + returnValue: _i19.Future>.value( + <_i4.Exercise>[], + ), + ) + as _i19.Future>); + + @override + void addListener(_i20.VoidCallback? listener) => super.noSuchMethod( + Invocation.method(#addListener, [listener]), + returnValueForMissingStub: null, + ); + + @override + void removeListener(_i20.VoidCallback? listener) => super.noSuchMethod( + Invocation.method(#removeListener, [listener]), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method(#dispose, []), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method(#notifyListeners, []), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [RoutinesProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRoutinesProvider extends _i1.Mock implements _i21.RoutinesProvider { + MockRoutinesProvider() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WgerBaseProvider get baseProvider => + (super.noSuchMethod( + Invocation.getter(#baseProvider), + returnValue: _FakeWgerBaseProvider_0( + this, + Invocation.getter(#baseProvider), + ), + ) + as _i2.WgerBaseProvider); + + @override + List<_i11.Routine> get items => + (super.noSuchMethod( + Invocation.getter(#items), + returnValue: <_i11.Routine>[], + ) + as List<_i11.Routine>); + + @override + List<_i9.WeightUnit> get weightUnits => + (super.noSuchMethod( + Invocation.getter(#weightUnits), + returnValue: <_i9.WeightUnit>[], + ) + as List<_i9.WeightUnit>); + + @override + _i9.WeightUnit get defaultWeightUnit => + (super.noSuchMethod( + Invocation.getter(#defaultWeightUnit), + returnValue: _FakeWeightUnit_7( + this, + Invocation.getter(#defaultWeightUnit), + ), + ) + as _i9.WeightUnit); + + @override + List<_i10.RepetitionUnit> get repetitionUnits => + (super.noSuchMethod( + Invocation.getter(#repetitionUnits), + returnValue: <_i10.RepetitionUnit>[], + ) + as List<_i10.RepetitionUnit>); + + @override + _i10.RepetitionUnit get defaultRepetitionUnit => + (super.noSuchMethod( + Invocation.getter(#defaultRepetitionUnit), + returnValue: _FakeRepetitionUnit_8( + this, + Invocation.getter(#defaultRepetitionUnit), + ), + ) + as _i10.RepetitionUnit); + + @override + set activeRoutine(_i11.Routine? value) => super.noSuchMethod( + Invocation.setter(#activeRoutine, value), + returnValueForMissingStub: null, + ); + + @override + set weightUnits(List<_i9.WeightUnit>? weightUnits) => super.noSuchMethod( + Invocation.setter(#weightUnits, weightUnits), + returnValueForMissingStub: null, + ); + + @override + set repetitionUnits(List<_i10.RepetitionUnit>? repetitionUnits) => super.noSuchMethod( + Invocation.setter(#repetitionUnits, repetitionUnits), + returnValueForMissingStub: null, + ); + + @override + bool get hasListeners => + (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) as bool); + + @override + void clear() => super.noSuchMethod( + Invocation.method(#clear, []), + returnValueForMissingStub: null, + ); + + @override + _i9.WeightUnit findWeightUnitById(int? id) => + (super.noSuchMethod( + Invocation.method(#findWeightUnitById, [id]), + returnValue: _FakeWeightUnit_7( + this, + Invocation.method(#findWeightUnitById, [id]), + ), + ) + as _i9.WeightUnit); + + @override + _i10.RepetitionUnit findRepetitionUnitById(int? id) => + (super.noSuchMethod( + Invocation.method(#findRepetitionUnitById, [id]), + returnValue: _FakeRepetitionUnit_8( + this, + Invocation.method(#findRepetitionUnitById, [id]), + ), + ) + as _i10.RepetitionUnit); + + @override + List<_i11.Routine> getPlans() => + (super.noSuchMethod( + Invocation.method(#getPlans, []), + returnValue: <_i11.Routine>[], + ) + as List<_i11.Routine>); + + @override + _i11.Routine findById(int? id) => + (super.noSuchMethod( + Invocation.method(#findById, [id]), + returnValue: _FakeRoutine_9( + this, + Invocation.method(#findById, [id]), + ), + ) + as _i11.Routine); + + @override + int findIndexById(int? id) => + (super.noSuchMethod( + Invocation.method(#findIndexById, [id]), + returnValue: 0, + ) + as int); + + @override + _i19.Future fetchAndSetAllRoutinesFull() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetAllRoutinesFull, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetAllRoutinesSparse() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetAllRoutinesSparse, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future setExercisesAndUnits( + List<_i22.DayData>? entries, { + Map? exercises, + }) => + (super.noSuchMethod( + Invocation.method( + #setExercisesAndUnits, + [entries], + {#exercises: exercises}, + ), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future<_i11.Routine> fetchAndSetRoutineSparse(int? planId) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetRoutineSparse, [planId]), + returnValue: _i19.Future<_i11.Routine>.value( + _FakeRoutine_9( + this, + Invocation.method(#fetchAndSetRoutineSparse, [planId]), + ), + ), + ) + as _i19.Future<_i11.Routine>); + + @override + _i19.Future<_i11.Routine> fetchAndSetRoutineFull(int? routineId) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetRoutineFull, [routineId]), + returnValue: _i19.Future<_i11.Routine>.value( + _FakeRoutine_9( + this, + Invocation.method(#fetchAndSetRoutineFull, [routineId]), + ), + ), + ) + as _i19.Future<_i11.Routine>); + + @override + _i19.Future<_i11.Routine> addRoutine(_i11.Routine? routine) => + (super.noSuchMethod( + Invocation.method(#addRoutine, [routine]), + returnValue: _i19.Future<_i11.Routine>.value( + _FakeRoutine_9(this, Invocation.method(#addRoutine, [routine])), + ), + ) + as _i19.Future<_i11.Routine>); + + @override + _i19.Future editRoutine(_i11.Routine? routine) => + (super.noSuchMethod( + Invocation.method(#editRoutine, [routine]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future deleteRoutine(int? id) => + (super.noSuchMethod( + Invocation.method(#deleteRoutine, [id]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetRepetitionUnits() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetRepetitionUnits, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetWeightUnits() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetWeightUnits, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetUnits() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetUnits, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future<_i12.Day> addDay(_i12.Day? day) => + (super.noSuchMethod( + Invocation.method(#addDay, [day]), + returnValue: _i19.Future<_i12.Day>.value( + _FakeDay_10(this, Invocation.method(#addDay, [day])), + ), + ) + as _i19.Future<_i12.Day>); + + @override + _i19.Future editDay(_i12.Day? day) => + (super.noSuchMethod( + Invocation.method(#editDay, [day]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future editDays(List<_i12.Day>? days) => + (super.noSuchMethod( + Invocation.method(#editDays, [days]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future deleteDay(int? dayId) => + (super.noSuchMethod( + Invocation.method(#deleteDay, [dayId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future<_i13.Slot> addSlot(_i13.Slot? slot, int? routineId) => + (super.noSuchMethod( + Invocation.method(#addSlot, [slot, routineId]), + returnValue: _i19.Future<_i13.Slot>.value( + _FakeSlot_11( + this, + Invocation.method(#addSlot, [slot, routineId]), + ), + ), + ) + as _i19.Future<_i13.Slot>); + + @override + _i19.Future deleteSlot(int? slotId, int? routineId) => + (super.noSuchMethod( + Invocation.method(#deleteSlot, [slotId, routineId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future editSlot(_i13.Slot? slot, int? routineId) => + (super.noSuchMethod( + Invocation.method(#editSlot, [slot, routineId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future editSlots(List<_i13.Slot>? slots, int? routineId) => + (super.noSuchMethod( + Invocation.method(#editSlots, [slots, routineId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future<_i14.SlotEntry> addSlotEntry( + _i14.SlotEntry? entry, + int? routineId, + ) => + (super.noSuchMethod( + Invocation.method(#addSlotEntry, [entry, routineId]), + returnValue: _i19.Future<_i14.SlotEntry>.value( + _FakeSlotEntry_12( + this, + Invocation.method(#addSlotEntry, [entry, routineId]), + ), + ), + ) + as _i19.Future<_i14.SlotEntry>); + + @override + _i19.Future deleteSlotEntry(int? id, int? routineId) => + (super.noSuchMethod( + Invocation.method(#deleteSlotEntry, [id, routineId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future editSlotEntry(_i14.SlotEntry? entry, int? routineId) => + (super.noSuchMethod( + Invocation.method(#editSlotEntry, [entry, routineId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + String getConfigUrl(_i14.ConfigType? type) => + (super.noSuchMethod( + Invocation.method(#getConfigUrl, [type]), + returnValue: _i23.dummyValue( + this, + Invocation.method(#getConfigUrl, [type]), + ), + ) + as String); + + @override + _i19.Future<_i15.BaseConfig> editConfig( + _i15.BaseConfig? config, + _i14.ConfigType? type, + ) => + (super.noSuchMethod( + Invocation.method(#editConfig, [config, type]), + returnValue: _i19.Future<_i15.BaseConfig>.value( + _FakeBaseConfig_13( + this, + Invocation.method(#editConfig, [config, type]), + ), + ), + ) + as _i19.Future<_i15.BaseConfig>); + + @override + _i19.Future<_i15.BaseConfig> addConfig( + _i15.BaseConfig? config, + _i14.ConfigType? type, + ) => + (super.noSuchMethod( + Invocation.method(#addConfig, [config, type]), + returnValue: _i19.Future<_i15.BaseConfig>.value( + _FakeBaseConfig_13( + this, + Invocation.method(#addConfig, [config, type]), + ), + ), + ) + as _i19.Future<_i15.BaseConfig>); + + @override + _i19.Future deleteConfig(int? id, _i14.ConfigType? type) => + (super.noSuchMethod( + Invocation.method(#deleteConfig, [id, type]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future handleConfig( + _i14.SlotEntry? entry, + num? value, + _i14.ConfigType? type, + ) => + (super.noSuchMethod( + Invocation.method(#handleConfig, [entry, value, type]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future> fetchSessionData() => + (super.noSuchMethod( + Invocation.method(#fetchSessionData, []), + returnValue: _i19.Future>.value( + <_i16.WorkoutSession>[], + ), + ) + as _i19.Future>); + + @override + _i19.Future<_i16.WorkoutSession> addSession( + _i16.WorkoutSession? session, + int? routineId, + ) => + (super.noSuchMethod( + Invocation.method(#addSession, [session, routineId]), + returnValue: _i19.Future<_i16.WorkoutSession>.value( + _FakeWorkoutSession_14( + this, + Invocation.method(#addSession, [session, routineId]), + ), + ), + ) + as _i19.Future<_i16.WorkoutSession>); + + @override + _i19.Future<_i16.WorkoutSession> editSession(_i16.WorkoutSession? session) => + (super.noSuchMethod( + Invocation.method(#editSession, [session]), + returnValue: _i19.Future<_i16.WorkoutSession>.value( + _FakeWorkoutSession_14( + this, + Invocation.method(#editSession, [session]), + ), + ), + ) + as _i19.Future<_i16.WorkoutSession>); + + @override + _i19.Future<_i17.Log> addLog(_i17.Log? log) => + (super.noSuchMethod( + Invocation.method(#addLog, [log]), + returnValue: _i19.Future<_i17.Log>.value( + _FakeLog_15(this, Invocation.method(#addLog, [log])), + ), + ) + as _i19.Future<_i17.Log>); + + @override + _i19.Future deleteLog(int? logId, int? routineId) => + (super.noSuchMethod( + Invocation.method(#deleteLog, [logId, routineId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + void addListener(_i20.VoidCallback? listener) => super.noSuchMethod( + Invocation.method(#addListener, [listener]), + returnValueForMissingStub: null, + ); + + @override + void removeListener(_i20.VoidCallback? listener) => super.noSuchMethod( + Invocation.method(#removeListener, [listener]), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method(#dispose, []), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method(#notifyListeners, []), + returnValueForMissingStub: null, + ); +}