mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Merge branch 'master' into feature/trophies
# Conflicts: # lib/providers/base_provider.dart # lib/providers/gym_state.dart # lib/providers/gym_state.g.dart # lib/widgets/routines/gym_mode/log_page.dart # test/core/settings_test.mocks.dart # test/exercises/contribute_exercise_image_test.mocks.dart # test/gallery/gallery_form_test.mocks.dart # test/gallery/gallery_screen_test.mocks.dart # test/measurements/measurement_provider_test.mocks.dart # test/nutrition/nutritional_plan_screen_test.mocks.dart # test/nutrition/nutritional_plans_screen_test.mocks.dart # test/routine/gym_mode/gym_mode_test.mocks.dart # test/routine/routine_screen_test.mocks.dart # test/routine/routines_provider_test.mocks.dart # test/routine/routines_screen_test.mocks.dart # test/user/provider_test.mocks.dart # test/weight/weight_provider_test.mocks.dart # test/widgets/routines/gym_mode/log_page_test.dart
This commit is contained in:
2
.github/actions/flutter-common/action.yml
vendored
2
.github/actions/flutter-common/action.yml
vendored
@@ -9,7 +9,7 @@ runs:
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3.38.5
|
||||
flutter-version: 3.38.6
|
||||
cache: true
|
||||
|
||||
- name: Install Flutter dependencies
|
||||
|
||||
2
.github/workflows/build-linux.yml
vendored
2
.github/workflows/build-linux.yml
vendored
@@ -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
|
||||
|
||||
23
Gemfile.lock
23
Gemfile.lock
@@ -8,8 +8,8 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1194.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.207.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)
|
||||
@@ -170,7 +171,7 @@ GEM
|
||||
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)
|
||||
@@ -179,7 +180,7 @@ GEM
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -84,6 +84,18 @@
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="1.9.5" date="2026-01-14">
|
||||
<description>
|
||||
<p>Bug fixes and improvements.</p>
|
||||
</description>
|
||||
<url>https://github.com/wger-project/flutter/releases/tag/1.9.5</url>
|
||||
</release>
|
||||
<release version="1.9.4" date="2025-12-23">
|
||||
<description>
|
||||
<p>Bug fixes and improvements.</p>
|
||||
</description>
|
||||
<url>https://github.com/wger-project/flutter/releases/tag/1.9.4</url>
|
||||
</release>
|
||||
<release version="1.9.3" date="2025-12-16">
|
||||
<description>
|
||||
<p>Bug fixes and improvements.</p>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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
|
||||
@@ -43,7 +43,8 @@ class WgerHttpException implements Exception {
|
||||
final dynamic responseBody = response.body;
|
||||
|
||||
final contentType = response.headers[HttpHeaders.contentTypeHeader];
|
||||
if (contentType != null && contentType.contains('text/html')) {
|
||||
if ((contentType != null && contentType.contains('text/html')) ||
|
||||
responseBody.toString().contains('<html')) {
|
||||
type = ErrorType.html;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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.
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'ingredients_database.dart';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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
|
||||
@@ -51,8 +51,6 @@ void showHttpExceptionErrorDialog(WgerHttpException exception, {BuildContext? co
|
||||
return;
|
||||
}
|
||||
|
||||
final theme = Theme.of(dialogContext);
|
||||
|
||||
showDialog(
|
||||
context: dialogContext,
|
||||
builder: (ctx) => AlertDialog(
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
|
||||
@@ -694,5 +694,551 @@
|
||||
"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": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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
|
||||
@@ -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,13 +101,13 @@ 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();
|
||||
|
||||
@@ -130,6 +130,51 @@ class Log {
|
||||
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;
|
||||
}
|
||||
|
||||
out.exerciseBase = exercise;
|
||||
out.repetitionUnit = repetitionsUnitObj;
|
||||
out.weightUnitObj = weightUnitObj;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Boilerplate
|
||||
factory Log.fromJson(Map<String, dynamic> json) => _$LogFromJson(json);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 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
|
||||
@@ -16,19 +16,27 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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: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;
|
||||
|
||||
@@ -60,20 +68,54 @@ class WgerBaseProvider {
|
||||
}
|
||||
|
||||
/// Fetch and retrieve the overview list of objects, returns the JSON parsed response
|
||||
Future<dynamic> fetch(Uri uri, {String? language}) async {
|
||||
// Send the request
|
||||
final response = await client.get(
|
||||
uri,
|
||||
headers: getDefaultHeaders(includeAuth: true),
|
||||
);
|
||||
/// with a simple retry mechanism for transient errors.
|
||||
Future<dynamic> fetch(
|
||||
Uri uri, {
|
||||
int maxRetries = 3,
|
||||
Duration initialDelay = const Duration(milliseconds: 250),
|
||||
String? language
|
||||
}) async {
|
||||
int attempt = 0;
|
||||
final random = math.Random();
|
||||
|
||||
// Something wrong with our request
|
||||
if (response.statusCode >= 400) {
|
||||
throw WgerHttpException(response);
|
||||
Future<void> 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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
|
||||
@@ -234,7 +234,12 @@ class ExercisesProvider with ChangeNotifier {
|
||||
|
||||
Future<void> 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<void> 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<void> 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<void> 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));
|
||||
|
||||
46
lib/providers/gym_log_state.dart
Normal file
46
lib/providers/gym_log_state.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
55
lib/providers/gym_log_state.g.dart
Normal file
55
lib/providers/gym_log_state.g.dart
Normal file
@@ -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<GymLogNotifier, Log?> {
|
||||
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<Log?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$gymLogNotifierHash() => r'4523975eeeaacceca4e86fb2e4ddd9a42c263d8e';
|
||||
|
||||
abstract class _$GymLogNotifier extends $Notifier<Log?> {
|
||||
Log? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<Log?, Log?>;
|
||||
final element =
|
||||
ref.element as $ClassProviderElement<AnyNotifier<Log?, Log?>, Log?, Object?, Object?>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2025 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
|
||||
@@ -25,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';
|
||||
|
||||
@@ -131,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,
|
||||
@@ -481,7 +487,7 @@ class GymStateNotifier extends _$GymStateNotifier {
|
||||
pages.add(PageEntry(type: PageType.workoutSummary, pageIndex: pageIndex + 1));
|
||||
|
||||
state = state.copyWith(pages: pages);
|
||||
// _logger.fine(readPageStructure());
|
||||
// print(readPageStructure());
|
||||
_logger.finer('Initialized ${state.pages.length} pages');
|
||||
}
|
||||
|
||||
@@ -573,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) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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<DashboardCalendarWidget>
|
||||
|
||||
// Process workout sessions
|
||||
final routinesProvider = context.read<RoutinesProvider>();
|
||||
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<NutritionPlansProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final nutritionProvider = context.read<NutritionPlansProvider>();
|
||||
for (final plan in nutritionProvider.items) {
|
||||
for (final entry in plan.logEntriesValues.entries) {
|
||||
final date = DateFormatLists.format(entry.key);
|
||||
|
||||
@@ -27,6 +27,7 @@ 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';
|
||||
@@ -39,75 +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<LogPage> {
|
||||
final GlobalKey<_LogFormWidgetState> _logFormKey = GlobalKey<_LogFormWidgetState>();
|
||||
|
||||
late FocusNode focusNode;
|
||||
// Persistent log and current slot-page id to avoid recreating the Log on rebuilds
|
||||
Log? _currentLog;
|
||||
String? _currentSlotPageUuid;
|
||||
|
||||
@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!;
|
||||
|
||||
// Create a Log only when the slot page changed or none exists yet
|
||||
if (_currentLog == null || _currentSlotPageUuid != slotEntryPage.uuid) {
|
||||
_currentLog = Log.fromSetConfigData(setConfigData)
|
||||
..routineId = state.routine.id!
|
||||
..iteration = state.iteration;
|
||||
_currentSlotPageUuid = slotEntryPage.uuid;
|
||||
} else {
|
||||
// Update routine/iteration if needed without creating a new Log
|
||||
_currentLog!
|
||||
..routineId = state.routine.id!
|
||||
..iteration = state.iteration;
|
||||
}
|
||||
|
||||
final log = _currentLog!;
|
||||
final log = ref.watch(gymLogProvider);
|
||||
|
||||
// Mark done sets
|
||||
final decorationStyle = slotEntryPage.logDone
|
||||
@@ -117,8 +80,9 @@ class _LogPageState extends ConsumerState<LogPage> {
|
||||
return Column(
|
||||
children: [
|
||||
NavigationHeader(
|
||||
log.exercise.getTranslation(languageCode).name,
|
||||
widget._controller,
|
||||
log!.exercise.getTranslation(languageCode).name,
|
||||
_controller,
|
||||
key: const ValueKey('log-page-navigation-header'),
|
||||
),
|
||||
|
||||
Container(
|
||||
@@ -164,16 +128,9 @@ class _LogPageState extends ConsumerState<LogPage> {
|
||||
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(),
|
||||
),
|
||||
@@ -186,16 +143,15 @@ class _LogPageState extends ConsumerState<LogPage> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: LogFormWidget(
|
||||
controller: widget._controller,
|
||||
controller: _controller,
|
||||
configData: setConfigData,
|
||||
log: log,
|
||||
focusNode: focusNode,
|
||||
// log: log!,
|
||||
key: _logFormKey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
NavigationFooter(widget._controller),
|
||||
NavigationFooter(_controller),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -255,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) {
|
||||
@@ -326,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);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -348,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) {
|
||||
@@ -427,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);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -453,22 +376,19 @@ class LogsWeightWidget extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class LogsPastLogsWidget extends StatelessWidget {
|
||||
final Log log;
|
||||
class LogsPastLogsWidget extends ConsumerWidget {
|
||||
final List<Log> 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(
|
||||
@@ -482,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),
|
||||
);
|
||||
@@ -516,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
|
||||
@@ -535,119 +442,11 @@ class _LogFormWidgetState extends ConsumerState<LogFormWidget> {
|
||||
final _form = GlobalKey<FormState>();
|
||||
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((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_syncControllersWithWidget();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant LogFormWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
// If the log or config changed, update internal _log and controllers
|
||||
if (oldWidget.log != widget.log || oldWidget.configData != widget.configData) {
|
||||
_log = widget.log;
|
||||
_syncControllersWithWidget();
|
||||
}
|
||||
}
|
||||
|
||||
void _syncControllersWithWidget() {
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
final numberFormat = NumberFormat.decimalPattern(locale);
|
||||
|
||||
// Priority: current log -> config defaults -> empty
|
||||
try {
|
||||
_repetitionsController.text = widget.log.repetitions != null
|
||||
? numberFormat.format(widget.log.repetitions)
|
||||
: (widget.configData.repetitions != null
|
||||
? numberFormat.format(widget.configData.repetitions)
|
||||
: '');
|
||||
|
||||
_weightController.text = widget.log.weight != null
|
||||
? numberFormat.format(widget.log.weight)
|
||||
: (widget.configData.weight != null ? numberFormat.format(widget.configData.weight) : '');
|
||||
} on Exception catch (e) {
|
||||
// Defensive fallback: set empty strings if formatting fails
|
||||
widget._logger.warning('Error syncing controllers: $e');
|
||||
_repetitionsController.text = '';
|
||||
_weightController.text = '';
|
||||
}
|
||||
}
|
||||
|
||||
@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.repetitions = pastLog.repetitions;
|
||||
_log.weight = pastLog.weight;
|
||||
_log.rir = pastLog.rir;
|
||||
if (pastLog.repetitionsUnitObj != null) {
|
||||
_log.repetitionUnit = pastLog.repetitionsUnitObj;
|
||||
}
|
||||
if (pastLog.weightUnitObj != null) {
|
||||
_log.weightUnit = pastLog.weightUnitObj;
|
||||
}
|
||||
|
||||
widget._logger.finer(
|
||||
'Copied to _log: repetitions=${_log.repetitions}, weight=${_log.weight}, repetitionsUnitId=${_log.repetitionsUnitId}, weightUnitId=${_log.weightUnitId}, rir=${_log.rir}',
|
||||
);
|
||||
|
||||
// Update plate calculator using the value currently visible in the controllers
|
||||
try {
|
||||
final weightValue = _weightController.text.isEmpty
|
||||
? 0
|
||||
: numberFormat.parse(_weightController.text);
|
||||
ref.read(plateCalculatorProvider.notifier).setWeight(weightValue);
|
||||
} catch (e) {
|
||||
widget._logger.fine('Error updating plate calculator: $e');
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure subsequent syncs (e.g., didUpdateWidget) don't overwrite these values
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_syncControllersWithWidget();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
final log = ref.watch(gymLogProvider);
|
||||
|
||||
return Form(
|
||||
key: _form,
|
||||
@@ -665,26 +464,14 @@ class _LogFormWidgetState extends ConsumerState<LogFormWidget> {
|
||||
Flexible(
|
||||
child: LogsRepsWidget(
|
||||
key: const ValueKey('logs-reps-widget'),
|
||||
controller: _repetitionsController,
|
||||
configData: widget.configData,
|
||||
focusNode: widget.focusNode,
|
||||
log: _log,
|
||||
setStateCallback: (fn) {
|
||||
setState(fn);
|
||||
},
|
||||
valueChange: widget.configData.repetitionsRounding,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: LogsWeightWidget(
|
||||
key: const ValueKey('logs-weight-widget'),
|
||||
controller: _weightController,
|
||||
configData: widget.configData,
|
||||
focusNode: widget.focusNode,
|
||||
log: _log,
|
||||
setStateCallback: (fn) {
|
||||
setState(fn);
|
||||
},
|
||||
valueChange: widget.configData.weightRounding,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -696,20 +483,14 @@ class _LogFormWidgetState extends ConsumerState<LogFormWidget> {
|
||||
Flexible(
|
||||
child: LogsRepsWidget(
|
||||
key: const ValueKey('logs-reps-widget'),
|
||||
controller: _repetitionsController,
|
||||
configData: widget.configData,
|
||||
focusNode: widget.focusNode,
|
||||
log: _log,
|
||||
setStateCallback: (fn) {
|
||||
setState(fn);
|
||||
},
|
||||
valueChange: widget.configData.repetitionsRounding,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: RepetitionUnitInputWidget(
|
||||
key: const ValueKey('repetition-unit-input-widget'),
|
||||
_log.repetitionsUnitId,
|
||||
log!.repetitionsUnitId,
|
||||
onChanged: (v) => {},
|
||||
),
|
||||
),
|
||||
@@ -723,19 +504,13 @@ class _LogFormWidgetState extends ConsumerState<LogFormWidget> {
|
||||
Flexible(
|
||||
child: LogsWeightWidget(
|
||||
key: const ValueKey('logs-weight-widget'),
|
||||
controller: _weightController,
|
||||
configData: widget.configData,
|
||||
focusNode: widget.focusNode,
|
||||
log: _log,
|
||||
setStateCallback: (fn) {
|
||||
setState(fn);
|
||||
},
|
||||
valueChange: widget.configData.weightRounding,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: WeightUnitInputWidget(
|
||||
_log.weightUnitId,
|
||||
log!.weightUnitId,
|
||||
onChanged: (v) => {},
|
||||
key: const ValueKey('weight-unit-input-widget'),
|
||||
),
|
||||
@@ -746,13 +521,9 @@ class _LogFormWidgetState extends ConsumerState<LogFormWidget> {
|
||||
if (_detailed)
|
||||
RiRInputWidget(
|
||||
key: const ValueKey('rir-input-widget'),
|
||||
_log.rir,
|
||||
log!.rir,
|
||||
onChanged: (value) {
|
||||
if (value == '') {
|
||||
_log.rir = null;
|
||||
} else {
|
||||
_log.rir = num.parse(value);
|
||||
}
|
||||
log.rir = value == '' ? null : num.parse(value);
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
@@ -785,7 +556,7 @@ class _LogFormWidgetState extends ConsumerState<LogFormWidget> {
|
||||
await provider.Provider.of<RoutinesProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).addLog(_log);
|
||||
).addLog(log!);
|
||||
final page = gymState.getSlotEntryPageByIndex()!;
|
||||
gymProvider.markSlotPageAsDone(page.uuid, isDone: true);
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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) {
|
||||
|
||||
124
pubspec.lock
124
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: "474d8355961658d43f1c976e2fa1ca715505bea1adbd56df34c581aaa70ec41f"
|
||||
sha256: bc7a96998258adddd0b653dd693b0874537707d58b0489708f2a646e4f124246
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.26+2"
|
||||
version: "0.6.27"
|
||||
camera_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_avfoundation
|
||||
sha256: "0efb057a1fecdbf9b697272fbf79afbd47ac0e7bd69b4d900d3f304b31d93bad"
|
||||
sha256: "087a9fadef20325cb246b4c13344a3ce8e408acfc3e0c665ebff0ec3144d7163"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.22+7"
|
||||
version: "0.9.22+8"
|
||||
camera_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -149,10 +141,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_web
|
||||
sha256: "3bc7bb1657a0f29c34116453c5d5e528c23efcf5e75aac0a3387cf108040bf65"
|
||||
sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+2"
|
||||
version: "0.3.5+3"
|
||||
carousel_slider:
|
||||
dependency: "direct main"
|
||||
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: "3669e1b68d7bffb60192ac6ba9fd2c0306804d7a00e5879f6364c69ecde53a7f"
|
||||
sha256: "5ea2f718558c0b31d4b8c36a3d8e5b7016f1265f46ceb5a5920e16117f0c0d6a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.30.0"
|
||||
version: "2.30.1"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: afe4d1d2cfce6606c86f11a6196e974a2ddbfaa992956ce61e054c9b1899c769
|
||||
sha256: "892dfb5d69d9e604bdcd102a9376de8b41768cf7be93fd26b63cfc4d8f91ad5f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.30.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: "51555e36056541237b15b57afc31a0f53d4f9aefd9bd00873a6dc0090e54e332"
|
||||
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
version: "4.7.2"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
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:
|
||||
@@ -1144,34 +1120,34 @@ 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:
|
||||
@@ -1333,10 +1309,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
sha256: "162435ede92bcc793ea939fdc0452eef0a73d11f8ed053b58a89792fba749da5"
|
||||
sha256: f52f5d5649dcc13ed198c4176ddef74bf6851c30f4f31603f1b37788695b93e2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.42.1"
|
||||
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: "8587f7b1e1ad7a7b8f7a7e153bd6de8607168f865f0bd983ef1f92efd3f4a02c"
|
||||
sha256: ee4fd520b0cafa02e4a867a0f882092e727cdaa1a2d24762171e787f8a502b0a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.0"
|
||||
version: "2.9.1"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
sha256: e4d33b79a064498c6eb3a6a492b6a5012573d4943c28d566caf1a6c0840fe78d
|
||||
sha256: d1eb970495a76abb35e5fa93ee3c58bd76fb6839e2ddf2fbb636674f2b971dd4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.8"
|
||||
version: "2.8.9"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1605,10 +1573,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
10
pubspec.yaml
10
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.3+130
|
||||
version: 1.9.5+150
|
||||
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
@@ -37,7 +37,7 @@ dependencies:
|
||||
collection: ^1.18.0
|
||||
cupertino_icons: ^1.0.8
|
||||
drift: ^2.30.0
|
||||
equatable: ^2.0.7
|
||||
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:
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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
|
||||
@@ -25,20 +25,24 @@ 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,
|
||||
@@ -46,13 +50,16 @@ void main() {
|
||||
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', () {
|
||||
test('detects HTML response from headers', () {
|
||||
// Arrange
|
||||
const body = '<html lang="en"><body>Error</body></html>';
|
||||
final resp = http.Response(
|
||||
body,
|
||||
@@ -60,16 +67,39 @@ void main() {
|
||||
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 = '<html lang="en"><body>Error</body></html>';
|
||||
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 = <String, dynamic>{'field': 'value'};
|
||||
|
||||
// Act
|
||||
final ex = WgerHttpException.fromMap(map);
|
||||
|
||||
// Assert
|
||||
expect(ex.type, ErrorType.json);
|
||||
expect(ex.errors, map);
|
||||
});
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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']));
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.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']));
|
||||
|
||||
135
test/providers/base_provider.dart
Normal file
135
test/providers/base_provider.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Map>());
|
||||
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<Map>());
|
||||
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<WgerHttpException>()),
|
||||
);
|
||||
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<ClientException>());
|
||||
// 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<Map>());
|
||||
expect(result['ok'], isTrue);
|
||||
verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(1);
|
||||
});
|
||||
}
|
||||
218
test/providers/base_provider.mocks.dart
Normal file
218
test/providers/base_provider.mocks.dart
Normal file
@@ -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<String, String>? 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<String, String>? 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<String, String>? 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<String, String>? 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<String, String>? 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<String, String>? 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<String> read(Uri? url, {Map<String, String>? headers}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#read, [url], {#headers: headers}),
|
||||
returnValue: _i3.Future<String>.value(
|
||||
_i5.dummyValue<String>(
|
||||
this,
|
||||
Invocation.method(#read, [url], {#headers: headers}),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i3.Future<String>);
|
||||
|
||||
@override
|
||||
_i3.Future<_i6.Uint8List> readBytes(
|
||||
Uri? url, {
|
||||
Map<String, String>? 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,
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,3 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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.
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Mocks generated by Mockito 5.4.6 from annotations
|
||||
// in wger/test/providers/plate_calculator_test.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* 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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@ void main() {
|
||||
|
||||
// Act
|
||||
notifier.calculatePages();
|
||||
notifier.setCurrentPage(2);
|
||||
|
||||
// Assert
|
||||
expect(notifier.state.getSlotEntryPageByIndex()!.type, SlotPageType.log);
|
||||
@@ -159,6 +160,7 @@ void main() {
|
||||
iteration: 1,
|
||||
);
|
||||
notifier.calculatePages();
|
||||
notifier.setCurrentPage(2);
|
||||
|
||||
// Act
|
||||
// Log page is at index 2
|
||||
@@ -189,15 +191,16 @@ void main() {
|
||||
|
||||
testWidgets('save button calls addLog on RoutinesProvider', (tester) async {
|
||||
// Arrange
|
||||
final gymNotifier = container.read(gymStateProvider.notifier);
|
||||
final notifier = container.read(gymStateProvider.notifier);
|
||||
final routine = testdata.getTestRoutine();
|
||||
gymNotifier.state = gymNotifier.state.copyWith(
|
||||
notifier.state = notifier.state.copyWith(
|
||||
dayId: routine.days.first.id,
|
||||
routine: routine,
|
||||
iteration: 1,
|
||||
);
|
||||
gymNotifier.calculatePages();
|
||||
gymNotifier.state = gymNotifier.state.copyWith(currentPage: 2);
|
||||
notifier.calculatePages();
|
||||
notifier.setCurrentPage(2);
|
||||
notifier.state = notifier.state.copyWith(currentPage: 2);
|
||||
final mockRoutines = MockRoutinesProvider();
|
||||
|
||||
// Act
|
||||
@@ -206,8 +209,8 @@ void main() {
|
||||
final editableFields = find.byType(EditableText);
|
||||
expect(editableFields, findsWidgets);
|
||||
|
||||
await tester.enterText(editableFields.at(0), '7');
|
||||
await tester.enterText(editableFields.at(1), '77');
|
||||
await tester.enterText(editableFields.at(0), '12'); // Reps
|
||||
await tester.enterText(editableFields.at(1), '34'); // Weight
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
Log? capturedLog;
|
||||
@@ -226,13 +229,13 @@ void main() {
|
||||
// Assert
|
||||
verify(mockRoutines.addLog(any)).called(1);
|
||||
expect(capturedLog, isNotNull);
|
||||
expect(capturedLog!.repetitions, equals(7));
|
||||
expect(capturedLog!.weight, equals(77));
|
||||
expect(capturedLog!.repetitions, equals(12));
|
||||
expect(capturedLog!.weight, equals(34));
|
||||
|
||||
final currentSlotPage = gymNotifier.state.getSlotEntryPageByIndex()!;
|
||||
final currentSlotPage = notifier.state.getSlotEntryPageByIndex()!;
|
||||
expect(capturedLog!.slotEntryId, equals(currentSlotPage.setConfigData!.slotEntryId));
|
||||
expect(capturedLog!.routineId, equals(gymNotifier.state.routine.id));
|
||||
expect(capturedLog!.iteration, equals(gymNotifier.state.iteration));
|
||||
expect(capturedLog!.routineId, equals(notifier.state.routine.id));
|
||||
expect(capturedLog!.iteration, equals(notifier.state.iteration));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user