From b806a6b51ce471eacf64a32c9fac68bc9cf33905 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:25:12 +0000 Subject: [PATCH 01/25] Bump aws-sdk-s3 from 1.176.0 to 1.208.0 in /ios Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.176.0 to 1.208.0. - [Release notes](https://github.com/aws/aws-sdk-ruby/releases) - [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-ruby/commits) --- updated-dependencies: - dependency-name: aws-sdk-s3 dependency-version: 1.208.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- ios/Gemfile.lock | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 5447a46b..b05f5052 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -9,24 +9,28 @@ GEM public_suffix (>= 2.0.2, < 7.0) artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.3.0) - aws-partitions (1.1018.0) - aws-sdk-core (3.214.0) + aws-eventstream (1.4.0) + aws-partitions (1.1196.0) + aws-sdk-core (3.240.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) + base64 + bigdecimal jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.96.0) - aws-sdk-core (~> 3, >= 3.210.0) + logger + aws-sdk-kms (1.118.0) + aws-sdk-core (~> 3, >= 3.239.1) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.176.0) - aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-s3 (1.208.0) + aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.10.1) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) + bigdecimal (4.0.1) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -157,6 +161,7 @@ GEM json (2.9.0) jwt (2.9.3) base64 + logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) From 0343a0c2a5634a074ffe794ac30e6d115e83dac7 Mon Sep 17 00:00:00 2001 From: Floris C Date: Fri, 19 Dec 2025 12:51:20 +0100 Subject: [PATCH 02/25] Translated using Weblate (Dutch) Currently translated at 100.0% (369 of 369 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/nl/ --- lib/l10n/app_nl.arb | 548 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 547 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 6da9385a..449807d2 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -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: {aliassen}", + "@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": {} } From 4bfcde044ff56f2bdb64961298aebcea72600e0d Mon Sep 17 00:00:00 2001 From: Justin Pinheiro Date: Fri, 19 Dec 2025 12:13:08 +0100 Subject: [PATCH 03/25] Translated using Weblate (French) Currently translated at 100.0% (369 of 369 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/fr/ --- lib/l10n/app_fr.arb | 64 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 902d4c58..429c9e54 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -7,7 +7,7 @@ "@weight": { "description": "The weight of a workout log or body weight entry" }, - "confirmDelete": "Êtes-vous sûre de vouloir supprimer « {toDelete} » ?", + "confirmDelete": "Êtes-vous sûr de vouloir supprimer '{toDelete}' ?", "@confirmDelete": { "description": "Confirmation text before the user deletes an object", "type": "text", @@ -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": {} } From a06dca5eee9101dccefb98cb5b40740f4e92968e Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Tue, 23 Dec 2025 15:50:32 +0100 Subject: [PATCH 04/25] Fix variable name --- lib/l10n/app_nl.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 449807d2..031e49e3 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -942,7 +942,7 @@ "@variations": { "description": "Variations of one exercise (e.g. benchpress and benchpress narrow)" }, - "alsoKnownAs": "Ook bekend als: {aliassen}", + "alsoKnownAs": "Ook bekend als: {aliases}", "@alsoKnownAs": { "placeholders": { "aliases": { From 54a2f0c2bc9e7b0f86291b3f030b86ddb56d2d1e Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Tue, 23 Dec 2025 15:58:18 +0100 Subject: [PATCH 05/25] Bump versions in Gemfile.lock --- Gemfile.lock | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index cf685475..1d7388fd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,8 +8,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1194.0) - aws-sdk-core (3.239.2) + aws-partitions (1.1198.0) + aws-sdk-core (3.240.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -20,7 +20,7 @@ GEM aws-sdk-kms (1.118.0) aws-sdk-core (~> 3, >= 3.239.1) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.207.0) + aws-sdk-s3 (1.208.0) aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -28,7 +28,7 @@ GEM 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) @@ -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) From ed5ab7613be81e02fbaf94139e2d9c5f8c3a3c1a Mon Sep 17 00:00:00 2001 From: Github-Actions Date: Tue, 23 Dec 2025 15:07:35 +0000 Subject: [PATCH 06/25] Bump version to 1.9.4 --- flatpak/de.wger.flutter.metainfo.xml | 6 ++++++ pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/flatpak/de.wger.flutter.metainfo.xml b/flatpak/de.wger.flutter.metainfo.xml index ab5aadb7..8560be10 100755 --- a/flatpak/de.wger.flutter.metainfo.xml +++ b/flatpak/de.wger.flutter.metainfo.xml @@ -84,6 +84,12 @@ + + +

Bug fixes and improvements.

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

Bug fixes and improvements.

diff --git a/pubspec.yaml b/pubspec.yaml index e70eb6c6..df97a997 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # - the version number is taken from the git tag vX.Y.Z # - the build number is computed by reading the last one from the play store # and increasing by one -version: 1.9.3+130 +version: 1.9.4+140 environment: sdk: '>=3.8.0 <4.0.0' From 994c962921ff3ec40f208bca1f18dd59d0b759cb Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sun, 28 Dec 2025 14:09:42 +0100 Subject: [PATCH 07/25] Bump flatpak-flutter version --- .github/workflows/build-linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 989036b0..b24ac7f3 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -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.10.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 From 16ea5233bc9bd9fed2935ea436317efcb50d829d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 00:03:33 +0000 Subject: [PATCH 08/25] Bump equatable from 2.0.7 to 2.0.8 Bumps [equatable](https://github.com/felangel/equatable) from 2.0.7 to 2.0.8. - [Release notes](https://github.com/felangel/equatable/releases) - [Changelog](https://github.com/felangel/equatable/blob/master/CHANGELOG.md) - [Commits](https://github.com/felangel/equatable/compare/v2.0.7...v2.0.8) --- updated-dependencies: - dependency-name: equatable dependency-version: 2.0.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 6b29145d..9cabd59f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -333,10 +333,10 @@ packages: 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: diff --git a/pubspec.yaml b/pubspec.yaml index df97a997..e28e0f82 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 From fb6a6735031ea96d74e8810f211b0033c1e65e62 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Mon, 12 Jan 2026 21:39:36 +0100 Subject: [PATCH 09/25] Simplify code by adding new log provider This makes the logic for copying or modifying the logs much easier. Also, there were some user reports that the old logic sometimes behaved erratically and old values were sometimes reverted. --- lib/models/workouts/log.dart | 53 ++- lib/models/workouts/session.g.dart | 4 +- lib/models/workouts/slot_data.g.dart | 18 - lib/providers/gym_log_state.dart | 46 ++ lib/providers/gym_log_state.g.dart | 56 +++ lib/providers/gym_state.dart | 23 +- lib/providers/gym_state.g.dart | 18 - lib/widgets/routines/gym_mode/log_page.dart | 444 +++++------------- lib/widgets/routines/gym_mode/navigation.dart | 11 +- test/core/validators_test.mocks.dart | 18 - test/routine/gym_mode/gym_mode_test.dart | 7 +- test/user/provider_test.mocks.dart | 18 - .../routines/gym_mode/log_page_test.dart | 13 +- .../gym_mode/log_page_test.mocks.dart | 18 - 14 files changed, 302 insertions(+), 445 deletions(-) create mode 100644 lib/providers/gym_log_state.dart create mode 100644 lib/providers/gym_log_state.g.dart diff --git a/lib/models/workouts/log.dart b/lib/models/workouts/log.dart index f4473aa1..52b5c9f6 100644 --- a/lib/models/workouts/log.dart +++ b/lib/models/workouts/log.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020 - 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 @@ -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 json) => _$LogFromJson(json); diff --git a/lib/models/workouts/session.g.dart b/lib/models/workouts/session.g.dart index ac78029d..bf061fa1 100644 --- a/lib/models/workouts/session.g.dart +++ b/lib/models/workouts/session.g.dart @@ -23,7 +23,9 @@ WorkoutSession _$WorkoutSessionFromJson(Map json) { id: (json['id'] as num?)?.toInt(), dayId: (json['day'] as num?)?.toInt(), routineId: (json['routine'] as num?)?.toInt(), - impression: json['impression'] == null ? 2 : int.parse(json['impression'] as String), + impression: json['impression'] == null + ? DEFAULT_IMPRESSION + : int.parse(json['impression'] as String), notes: json['notes'] as String? ?? '', timeStart: stringToTimeNull(json['time_start'] as String?), timeEnd: stringToTimeNull(json['time_end'] as String?), diff --git a/lib/models/workouts/slot_data.g.dart b/lib/models/workouts/slot_data.g.dart index 589b2b99..756717e6 100644 --- a/lib/models/workouts/slot_data.g.dart +++ b/lib/models/workouts/slot_data.g.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * 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 . - */ - // GENERATED CODE - DO NOT MODIFY BY HAND part of 'slot_data.dart'; diff --git a/lib/providers/gym_log_state.dart b/lib/providers/gym_log_state.dart new file mode 100644 index 00000000..96604c5b --- /dev/null +++ b/lib/providers/gym_log_state.dart @@ -0,0 +1,46 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:wger/models/workouts/log.dart'; + +part 'gym_log_state.g.dart'; + +@Riverpod(keepAlive: true) +class GymLogNotifier extends _$GymLogNotifier { + final _logger = Logger('GymLogNotifier'); + + @override + Log? build() { + _logger.finer('Initializing GymLogNotifier'); + return null; + } + + void setLog(Log newLog) { + state = newLog; + } + + void setWeight(num weight) { + state = state?.copyWith(weight: weight); + } + + void setRepetitions(num repetitions) { + state = state?.copyWith(repetitions: repetitions); + } +} diff --git a/lib/providers/gym_log_state.g.dart b/lib/providers/gym_log_state.g.dart new file mode 100644 index 00000000..83c92cc1 --- /dev/null +++ b/lib/providers/gym_log_state.g.dart @@ -0,0 +1,56 @@ +// 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) +const gymLogProvider = GymLogNotifierProvider._(); + +final class GymLogNotifierProvider extends $NotifierProvider { + const GymLogNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'gymLogProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$gymLogNotifierHash(); + + @$internal + @override + GymLogNotifier create() => GymLogNotifier(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Log? value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$gymLogNotifierHash() => r'f7cdc8f72506e366ca028360b654da0bdd9bcae6'; + +abstract class _$GymLogNotifier extends $Notifier { + Log? build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element as $ClassProviderElement, Log?, Object?, Object?>; + element.handleValue(ref, created); + } +} diff --git a/lib/providers/gym_state.dart b/lib/providers/gym_state.dart index 949a753f..882df366 100644 --- a/lib/providers/gym_state.dart +++ b/lib/providers/gym_state.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * 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); - print(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) { diff --git a/lib/providers/gym_state.g.dart b/lib/providers/gym_state.g.dart index 7596fa4b..1899e808 100644 --- a/lib/providers/gym_state.g.dart +++ b/lib/providers/gym_state.g.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * 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 . - */ - // GENERATED CODE - DO NOT MODIFY BY HAND part of 'gym_state.dart'; diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index 70254266..088836d6 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020 - 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -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 { 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 { 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 { 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 { 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 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,116 +442,11 @@ class _LogFormWidgetState extends ConsumerState { final _form = GlobalKey(); var _detailed = false; bool _isSaving = false; - late Log _log; - - late final TextEditingController _repetitionsController; - late final TextEditingController _weightController; - - @override - void initState() { - super.initState(); - - _log = widget.log; - _repetitionsController = TextEditingController(); - _weightController = TextEditingController(); - - WidgetsBinding.instance.addPostFrameCallback((_) { - _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, @@ -662,26 +464,14 @@ class _LogFormWidgetState extends ConsumerState { 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, ), ), ], @@ -693,20 +483,14 @@ class _LogFormWidgetState extends ConsumerState { 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) => {}, ), ), @@ -720,19 +504,13 @@ class _LogFormWidgetState extends ConsumerState { 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'), ), @@ -743,13 +521,9 @@ class _LogFormWidgetState extends ConsumerState { 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( @@ -782,7 +556,7 @@ class _LogFormWidgetState extends ConsumerState { await provider.Provider.of( context, listen: false, - ).addLog(_log); + ).addLog(log!); final page = gymState.getSlotEntryPageByIndex()!; gymProvider.markSlotPageAsDone(page.uuid, isDone: true); diff --git a/lib/widgets/routines/gym_mode/navigation.dart b/lib/widgets/routines/gym_mode/navigation.dart index d2572b52..9639c3d0 100644 --- a/lib/widgets/routines/gym_mode/navigation.dart +++ b/lib/widgets/routines/gym_mode/navigation.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -28,7 +28,12 @@ class NavigationHeader extends StatelessWidget { final String _title; final bool showEndWorkoutButton; - const NavigationHeader(this._title, this._controller, {this.showEndWorkoutButton = true}); + const NavigationHeader( + this._title, + this._controller, { + this.showEndWorkoutButton = true, + super.key, + }); @override Widget build(BuildContext context) { diff --git a/test/core/validators_test.mocks.dart b/test/core/validators_test.mocks.dart index 8791d455..9b069055 100644 --- a/test/core/validators_test.mocks.dart +++ b/test/core/validators_test.mocks.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * 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 . - */ - // Mocks generated by Mockito 5.4.6 from annotations // in wger/test/core/validators_test.dart. // Do not manually edit this file. diff --git a/test/routine/gym_mode/gym_mode_test.dart b/test/routine/gym_mode/gym_mode_test.dart index 38a3e296..3f61e88b 100644 --- a/test/routine/gym_mode/gym_mode_test.dart +++ b/test/routine/gym_mode/gym_mode_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020, 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -109,12 +109,10 @@ void main() { await withClock(Clock.fixed(DateTime(2025, 3, 29, 14, 33)), () async { await tester.pumpWidget(renderGymMode()); - await tester.pumpAndSettle(); await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); - //await tester.ensureVisible(find.byKey(Key(key as String))); + // // Start page // @@ -306,6 +304,7 @@ void main() { expect(find.byIcon(Icons.chevron_right), findsNothing); }); }, + tags: ['golden'], semanticsEnabled: false, ); } diff --git a/test/user/provider_test.mocks.dart b/test/user/provider_test.mocks.dart index b8fce543..08bbf636 100644 --- a/test/user/provider_test.mocks.dart +++ b/test/user/provider_test.mocks.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * 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 . - */ - // Mocks generated by Mockito 5.4.6 from annotations // in wger/test/user/provider_test.dart. // Do not manually edit this file. diff --git a/test/widgets/routines/gym_mode/log_page_test.dart b/test/widgets/routines/gym_mode/log_page_test.dart index 45526e7f..b9ba0f8d 100644 --- a/test/widgets/routines/gym_mode/log_page_test.dart +++ b/test/widgets/routines/gym_mode/log_page_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2025 - 2025 wger Team + * Copyright (c) 2025 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -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 @@ -197,6 +199,7 @@ void main() { iteration: 1, ); notifier.calculatePages(); + notifier.setCurrentPage(2); notifier.state = notifier.state.copyWith(currentPage: 2); final mockRoutines = MockRoutinesProvider(); @@ -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,8 +229,8 @@ 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 = notifier.state.getSlotEntryPageByIndex()!; expect(capturedLog!.slotEntryId, equals(currentSlotPage.setConfigData!.slotEntryId)); diff --git a/test/widgets/routines/gym_mode/log_page_test.mocks.dart b/test/widgets/routines/gym_mode/log_page_test.mocks.dart index 500c61d8..e9105c48 100644 --- a/test/widgets/routines/gym_mode/log_page_test.mocks.dart +++ b/test/widgets/routines/gym_mode/log_page_test.mocks.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * 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 . - */ - // Mocks generated by Mockito 5.4.6 from annotations // in wger/test/widgets/routines/gym_mode/log_page_test.dart. // Do not manually edit this file. From cbdc4a0c561b438f9ef3d1ec43048d1944a69baa Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Mon, 12 Jan 2026 21:56:59 +0100 Subject: [PATCH 10/25] Increase page size for languages, no need to use pagination for this Categories and muscles will probably never get so big, but it doesn't do any harm doing it as well. --- lib/providers/exercises.dart | 30 +++++++++++++++---- test/exercises/exercise_provider_db_test.dart | 30 +++++++++++++++++-- test/exercises/exercise_provider_test.dart | 30 +++++++++++++++++-- 3 files changed, 79 insertions(+), 11 deletions(-) diff --git a/lib/providers/exercises.dart b/lib/providers/exercises.dart index 50c8175c..4e87cf16 100644 --- a/lib/providers/exercises.dart +++ b/lib/providers/exercises.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -234,7 +234,12 @@ class ExercisesProvider with ChangeNotifier { Future fetchAndSetCategoriesFromApi() async { _logger.info('Loading exercise categories from API'); - final categories = await baseProvider.fetchPaginated(baseProvider.makeUrl(categoriesUrlPath)); + final categories = await baseProvider.fetchPaginated( + baseProvider.makeUrl( + categoriesUrlPath, + query: {'limit': API_MAX_PAGE_SIZE}, + ), + ); for (final category in categories) { _categories.add(ExerciseCategory.fromJson(category)); } @@ -242,7 +247,12 @@ class ExercisesProvider with ChangeNotifier { Future fetchAndSetMusclesFromApi() async { _logger.info('Loading muscles from API'); - final muscles = await baseProvider.fetchPaginated(baseProvider.makeUrl(musclesUrlPath)); + final muscles = await baseProvider.fetchPaginated( + baseProvider.makeUrl( + musclesUrlPath, + query: {'limit': API_MAX_PAGE_SIZE}, + ), + ); for (final muscle in muscles) { _muscles.add(Muscle.fromJson(muscle)); @@ -251,7 +261,12 @@ class ExercisesProvider with ChangeNotifier { Future fetchAndSetEquipmentsFromApi() async { _logger.info('Loading equipment from API'); - final equipments = await baseProvider.fetchPaginated(baseProvider.makeUrl(equipmentUrlPath)); + final equipments = await baseProvider.fetchPaginated( + baseProvider.makeUrl( + equipmentUrlPath, + query: {'limit': API_MAX_PAGE_SIZE}, + ), + ); for (final equipment in equipments) { _equipment.add(Equipment.fromJson(equipment)); @@ -261,7 +276,12 @@ class ExercisesProvider with ChangeNotifier { Future fetchAndSetLanguagesFromApi() async { _logger.info('Loading languages from API'); - final languageData = await baseProvider.fetchPaginated(baseProvider.makeUrl(languageUrlPath)); + final languageData = await baseProvider.fetchPaginated( + baseProvider.makeUrl( + languageUrlPath, + query: {'limit': API_MAX_PAGE_SIZE}, + ), + ); for (final language in languageData) { _languages.add(Language.fromJson(language)); diff --git a/test/exercises/exercise_provider_db_test.dart b/test/exercises/exercise_provider_db_test.dart index 3862baa6..2d0c1f8b 100644 --- a/test/exercises/exercise_provider_db_test.dart +++ b/test/exercises/exercise_provider_db_test.dart @@ -1,3 +1,21 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import 'dart:convert'; import 'package:drift/native.dart'; @@ -100,19 +118,25 @@ void main() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); // Mock categories - when(mockBaseProvider.makeUrl(categoryUrl)).thenReturn(tCategoryEntriesUri); + when( + mockBaseProvider.makeUrl(categoryUrl, query: anyNamed('query')), + ).thenReturn(tCategoryEntriesUri); when( mockBaseProvider.fetchPaginated(tCategoryEntriesUri), ).thenAnswer((_) => Future.value(tCategoryMap['results'])); // Mock muscles - when(mockBaseProvider.makeUrl(muscleUrl)).thenReturn(tMuscleEntriesUri); + when( + mockBaseProvider.makeUrl(muscleUrl, query: anyNamed('query')), + ).thenReturn(tMuscleEntriesUri); when( mockBaseProvider.fetchPaginated(tMuscleEntriesUri), ).thenAnswer((_) => Future.value(tMuscleMap['results'])); // Mock equipment - when(mockBaseProvider.makeUrl(equipmentUrl)).thenReturn(tEquipmentEntriesUri); + when( + mockBaseProvider.makeUrl(equipmentUrl, query: anyNamed('query')), + ).thenReturn(tEquipmentEntriesUri); when( mockBaseProvider.fetchPaginated(tEquipmentEntriesUri), ).thenAnswer((_) => Future.value(tEquipmentMap['results'])); diff --git a/test/exercises/exercise_provider_test.dart b/test/exercises/exercise_provider_test.dart index f0920635..d597759c 100644 --- a/test/exercises/exercise_provider_test.dart +++ b/test/exercises/exercise_provider_test.dart @@ -1,3 +1,21 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import 'dart:convert'; import 'package:drift/drift.dart'; @@ -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'])); From 814a356e14dadf6d36e5c356ae9a4ca2a4e79889 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:15:20 +0000 Subject: [PATCH 11/25] Bump flutter_riverpod, riverpod_generator and riverpod_annotation Bumps [flutter_riverpod](https://github.com/rrousselGit/riverpod), [riverpod_generator](https://github.com/rrousselGit/riverpod) and [riverpod_annotation](https://github.com/rrousselGit/riverpod). These dependencies needed to be updated together. Updates `flutter_riverpod` from 3.0.3 to 3.1.0 - [Commits](https://github.com/rrousselGit/riverpod/compare/flutter_riverpod-v3.0.3...flutter_riverpod-v3.1.0) Updates `riverpod_generator` from 3.0.3 to 4.0.0+1 - [Commits](https://github.com/rrousselGit/riverpod/compare/riverpod_generator-v3.0.3...riverpod_generator-v4.0.0) Updates `riverpod_annotation` from 3.0.3 to 4.0.0 - [Commits](https://github.com/rrousselGit/riverpod/compare/riverpod_annotation-v3.0.3...riverpod_annotation-v4.0.0) --- updated-dependencies: - dependency-name: flutter_riverpod dependency-version: 3.1.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: riverpod_generator dependency-version: 4.0.0+1 dependency-type: direct:production update-type: version-update:semver-major - dependency-name: riverpod_annotation dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pubspec.lock | 52 ++++++++++------------------------------------------ pubspec.yaml | 6 +++--- 2 files changed, 13 insertions(+), 45 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 6b29145d..40fa1216 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index df97a997..f2d532c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 From 4782c729348175651976ffa2a6dd1536d07a93f9 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Tue, 13 Jan 2026 16:26:17 +0100 Subject: [PATCH 12/25] Recreate generated files --- .../ingredients/ingredients_database.g.dart | 18 ------------------ lib/providers/gym_log_state.g.dart | 9 ++++----- lib/providers/gym_state.g.dart | 9 ++++----- .../providers/plate_calculator_test.mocks.dart | 18 ------------------ 4 files changed, 8 insertions(+), 46 deletions(-) diff --git a/lib/database/ingredients/ingredients_database.g.dart b/lib/database/ingredients/ingredients_database.g.dart index 4b73341b..44ebcde2 100644 --- a/lib/database/ingredients/ingredients_database.g.dart +++ b/lib/database/ingredients/ingredients_database.g.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * 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 . - */ - // GENERATED CODE - DO NOT MODIFY BY HAND part of 'ingredients_database.dart'; diff --git a/lib/providers/gym_log_state.g.dart b/lib/providers/gym_log_state.g.dart index 83c92cc1..b2d45a45 100644 --- a/lib/providers/gym_log_state.g.dart +++ b/lib/providers/gym_log_state.g.dart @@ -10,10 +10,10 @@ part of 'gym_log_state.dart'; // ignore_for_file: type=lint, type=warning @ProviderFor(GymLogNotifier) -const gymLogProvider = GymLogNotifierProvider._(); +final gymLogProvider = GymLogNotifierProvider._(); final class GymLogNotifierProvider extends $NotifierProvider { - const GymLogNotifierProvider._() + GymLogNotifierProvider._() : super( from: null, argument: null, @@ -40,17 +40,16 @@ final class GymLogNotifierProvider extends $NotifierProvider r'f7cdc8f72506e366ca028360b654da0bdd9bcae6'; +String _$gymLogNotifierHash() => r'4523975eeeaacceca4e86fb2e4ddd9a42c263d8e'; abstract class _$GymLogNotifier extends $Notifier { Log? build(); @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element as $ClassProviderElement, Log?, Object?, Object?>; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } diff --git a/lib/providers/gym_state.g.dart b/lib/providers/gym_state.g.dart index 1899e808..4239a549 100644 --- a/lib/providers/gym_state.g.dart +++ b/lib/providers/gym_state.g.dart @@ -10,10 +10,10 @@ part of 'gym_state.dart'; // ignore_for_file: type=lint, type=warning @ProviderFor(GymStateNotifier) -const gymStateProvider = GymStateNotifierProvider._(); +final gymStateProvider = GymStateNotifierProvider._(); final class GymStateNotifierProvider extends $NotifierProvider { - const GymStateNotifierProvider._() + GymStateNotifierProvider._() : super( from: null, argument: null, @@ -40,14 +40,13 @@ final class GymStateNotifierProvider extends $NotifierProvider r'4e1ac85de3c9f5c7dad4b0c5e6ad80ad36397610'; +String _$gymStateNotifierHash() => r'3a0bb78e9f7e682ba93a40a73b170126b5eb5ca9'; abstract class _$GymStateNotifier extends $Notifier { GymModeState build(); @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -57,6 +56,6 @@ abstract class _$GymStateNotifier extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } diff --git a/test/providers/plate_calculator_test.mocks.dart b/test/providers/plate_calculator_test.mocks.dart index 1036371d..9b3fed8e 100644 --- a/test/providers/plate_calculator_test.mocks.dart +++ b/test/providers/plate_calculator_test.mocks.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * 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 . - */ - // Mocks generated by Mockito 5.4.6 from annotations // in wger/test/providers/plate_calculator_test.dart. // Do not manually edit this file. From 43427cbf8affeaea59ddbbe7517ad8a6b637a870 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Tue, 13 Jan 2026 16:18:25 +0100 Subject: [PATCH 13/25] Add riverpod linting --- analysis_options.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index b7abc707..79c8d7ca 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -14,15 +14,16 @@ analyzer: # Allow self-reference to deprecated members (we do this because otherwise we have # to annotate every member in every test, assert, etc, when we deprecate something) deprecated_member_use_from_same_package: ignore - # Ignore analyzer hints for updating pubspecs when using Future or - # Stream and not importing dart:async - # Please see https://github.com/flutter/flutter/pull/24528 for details. - sdk_version_async_exported_from_core: ignore + plugins: + - riverpod_lint formatter: page_width: 100 trailing_commas: preserve +plugins: + riverpod_lint: 3.1.0 + linter: rules: # These rules are documented on and in the same order as From 64b0ce3cc684ea453e83472b6dce749c1fb56671 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Tue, 13 Jan 2026 16:31:06 +0100 Subject: [PATCH 14/25] Bump flutter version --- .github/actions/flutter-common/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/flutter-common/action.yml b/.github/actions/flutter-common/action.yml index 5592f5d5..ee3b35f9 100644 --- a/.github/actions/flutter-common/action.yml +++ b/.github/actions/flutter-common/action.yml @@ -9,7 +9,7 @@ runs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.5 + flutter-version: 3.38.6 cache: true - name: Install Flutter dependencies From 5844a370d3b1055dbc06ac885ca1864d0d62b63f Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Tue, 13 Jan 2026 16:32:21 +0100 Subject: [PATCH 15/25] Upgrade transitive packages --- pubspec.lock | 68 ++++++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index cfb098ba..77750a63 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: analyzer - sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 url: "https://pub.dev" source: hosted - version: "8.4.0" + version: "8.4.1" analyzer_buffer: dependency: transitive description: @@ -61,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: @@ -85,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: @@ -101,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: @@ -117,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: @@ -141,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: @@ -221,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: @@ -293,18 +293,18 @@ 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: @@ -325,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: @@ -643,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: @@ -856,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: @@ -1309,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: @@ -1533,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: @@ -1573,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: From 574ef3d0b591856be7c03088d3a343257d9fb29e Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Wed, 14 Jan 2026 13:59:41 +0100 Subject: [PATCH 16/25] Add simple retry logic to the base provider's fetch method This should take care of simple transient errors, or other network hiccups that might happen on the user's device. --- lib/providers/base_provider.dart | 66 ++++-- test/core/settings_test.mocks.dart | 12 +- .../contribute_exercise_image_test.mocks.dart | 12 +- test/gallery/gallery_form_test.mocks.dart | 12 +- test/gallery/gallery_screen_test.mocks.dart | 12 +- .../measurement_provider_test.mocks.dart | 12 +- .../nutritional_plan_screen_test.mocks.dart | 12 +- .../nutritional_plans_screen_test.mocks.dart | 12 +- test/providers/base_provider.dart | 135 +++++++++++ test/providers/base_provider.mocks.dart | 218 ++++++++++++++++++ .../routine/gym_mode/gym_mode_test.mocks.dart | 12 +- test/routine/routine_screen_test.mocks.dart | 12 +- .../routine/routines_provider_test.mocks.dart | 12 +- test/routine/routines_screen_test.mocks.dart | 12 +- test/user/provider_test.mocks.dart | 12 +- test/weight/weight_provider_test.mocks.dart | 12 +- 16 files changed, 536 insertions(+), 39 deletions(-) create mode 100644 test/providers/base_provider.dart create mode 100644 test/providers/base_provider.mocks.dart diff --git a/lib/providers/base_provider.dart b/lib/providers/base_provider.dart index dee257d7..b905e29c 100644 --- a/lib/providers/base_provider.dart +++ b/lib/providers/base_provider.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,19 +16,27 @@ * along with this program. If not, see . */ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:math' as math; import 'package:http/http.dart' as http; import 'package:http/http.dart'; +import 'package:logging/logging.dart'; import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/providers/auth.dart'; import 'package:wger/providers/helpers.dart'; +/// initial delay for fetch retries, in milliseconds +const FETCH_INITIAL_DELAY = 250; + /// Base provider class. /// /// Provides a couple of comfort functions so we avoid a bit of boilerplate. class WgerBaseProvider { + final _logger = Logger('WgerBaseProvider'); + AuthProvider auth; late http.Client client; @@ -56,21 +64,53 @@ class WgerBaseProvider { } /// Fetch and retrieve the overview list of objects, returns the JSON parsed response - Future fetch(Uri uri) async { - // Future | List> fetch(Uri uri) async { - // Send the request - final response = await client.get( - uri, - headers: getDefaultHeaders(includeAuth: true), - ); + /// with a simple retry mechanism for transient errors. + Future fetch( + Uri uri, { + int maxRetries = 3, + Duration initialDelay = const Duration(milliseconds: 250), + }) async { + int attempt = 0; + final random = math.Random(); - // Something wrong with our request - if (response.statusCode >= 400) { - throw WgerHttpException(response); + Future wait(String reason) async { + final backoff = (initialDelay.inMilliseconds * math.pow(2, attempt - 1)).toInt(); + final jitter = random.nextInt((backoff * 0.25).toInt() + 1); // up to 25% jitter + final delay = backoff + jitter; + _logger.info('Retrying fetch for $uri, attempt $attempt (${delay}ms), reason: $reason'); + + await Future.delayed(Duration(milliseconds: delay)); } - // Process the response - return json.decode(utf8.decode(response.bodyBytes)) as dynamic; + while (true) { + try { + final response = await client + .get(uri, headers: getDefaultHeaders(includeAuth: true)) + .timeout(const Duration(seconds: 5)); + + if (response.statusCode >= 400) { + // Retry on server errors (5xx); e.g. 502 might be transient + if (response.statusCode >= 500 && attempt < maxRetries) { + attempt++; + await wait('status code ${response.statusCode}'); + continue; + } + throw WgerHttpException(response); + } + + return json.decode(utf8.decode(response.bodyBytes)) as dynamic; + } catch (e) { + final isRetryable = + e is SocketException || e is http.ClientException || e is TimeoutException; + if (isRetryable && attempt < maxRetries) { + attempt++; + await wait(e.toString()); + continue; + } + + rethrow; + } + } } /// Fetch and retrieve the overview list of objects, returns the JSON parsed response diff --git a/test/core/settings_test.mocks.dart b/test/core/settings_test.mocks.dart index db2394bd..699e32d1 100644 --- a/test/core/settings_test.mocks.dart +++ b/test/core/settings_test.mocks.dart @@ -1100,9 +1100,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { as Uri); @override - _i18.Future fetch(Uri? uri) => + _i18.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i18.Future.value(), ) as _i18.Future); diff --git a/test/exercises/contribute_exercise_image_test.mocks.dart b/test/exercises/contribute_exercise_image_test.mocks.dart index c3685ed3..b477e9d3 100644 --- a/test/exercises/contribute_exercise_image_test.mocks.dart +++ b/test/exercises/contribute_exercise_image_test.mocks.dart @@ -402,9 +402,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { as Uri); @override - _i14.Future fetch(Uri? uri) => + _i14.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i14.Future.value(), ) as _i14.Future); diff --git a/test/gallery/gallery_form_test.mocks.dart b/test/gallery/gallery_form_test.mocks.dart index e14d7838..3ec16dfa 100644 --- a/test/gallery/gallery_form_test.mocks.dart +++ b/test/gallery/gallery_form_test.mocks.dart @@ -175,9 +175,17 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { as Uri); @override - _i6.Future fetch(Uri? uri) => + _i6.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i6.Future.value(), ) as _i6.Future); diff --git a/test/gallery/gallery_screen_test.mocks.dart b/test/gallery/gallery_screen_test.mocks.dart index d1ab1318..c9fe9e74 100644 --- a/test/gallery/gallery_screen_test.mocks.dart +++ b/test/gallery/gallery_screen_test.mocks.dart @@ -175,9 +175,17 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { as Uri); @override - _i6.Future fetch(Uri? uri) => + _i6.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i6.Future.value(), ) as _i6.Future); diff --git a/test/measurements/measurement_provider_test.mocks.dart b/test/measurements/measurement_provider_test.mocks.dart index ef362a87..96f42b39 100644 --- a/test/measurements/measurement_provider_test.mocks.dart +++ b/test/measurements/measurement_provider_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/nutrition/nutritional_plan_screen_test.mocks.dart b/test/nutrition/nutritional_plan_screen_test.mocks.dart index 2fe3c4ac..3ee7049a 100644 --- a/test/nutrition/nutritional_plan_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plan_screen_test.mocks.dart @@ -122,9 +122,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/nutrition/nutritional_plans_screen_test.mocks.dart b/test/nutrition/nutritional_plans_screen_test.mocks.dart index c702d401..5c9dae55 100644 --- a/test/nutrition/nutritional_plans_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plans_screen_test.mocks.dart @@ -357,9 +357,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i8.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/providers/base_provider.dart b/test/providers/base_provider.dart new file mode 100644 index 00000000..203973c3 --- /dev/null +++ b/test/providers/base_provider.dart @@ -0,0 +1,135 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; +import 'package:wger/providers/base_provider.dart'; + +import '../utils.dart'; +import 'base_provider.mocks.dart'; + +@GenerateMocks([Client]) +void main() { + final Uri testUri = Uri(scheme: 'https', host: 'localhost', path: 'api/v2/test/'); + + test('Retry on SocketException then succeeds', () async { + // Arrange + final mockClient = MockClient(); + var callCount = 0; + when(mockClient.get(testUri, headers: anyNamed('headers'))).thenAnswer((_) { + if (callCount == 0) { + callCount++; + return Future.error(const SocketException('conn fail')); + } + return Future.value(Response('{"ok": true}', 200)); + }); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + final result = await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)); + + // Assert + expect(result, isA()); + expect(result['ok'], isTrue); + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(2); + }); + + test('Retry on 5xx then succeeds', () async { + // Arrange + final mockClient = MockClient(); + var callCount = 0; + when(mockClient.get(testUri, headers: anyNamed('headers'))).thenAnswer((_) { + if (callCount == 0) { + callCount++; + return Future.value(Response('{"msg":"error"}', 502)); + } + return Future.value(Response('{"ok": true}', 200)); + }); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + final result = await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)); + + // Assert + expect(result, isA()); + expect(result['ok'], isTrue); + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(2); + }); + + test('Do not retry on 4xx client error', () async { + // Arrange + final mockClient = MockClient(); + when( + mockClient.get(testUri, headers: anyNamed('headers')), + ).thenAnswer((_) => Future.value(Response('{"error":"bad"}', 400))); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + + // Assert + await expectLater( + provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)), + throwsA(isA()), + ); + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(1); + }); + + test('Exceed max retries and rethrow after retries', () async { + // Arrange + final mockClient = MockClient(); + when( + mockClient.get(testUri, headers: anyNamed('headers')), + ).thenAnswer((_) => Future.error(ClientException('conn fail'))); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + dynamic caught; + try { + await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)); + } catch (e) { + caught = e; + } + + // Assert + expect(caught, isA()); + // initial try + 3 retries = 4 calls + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(4); + }); + + test('Request succeeds without retries', () async { + // Arrange + final mockClient = MockClient(); + when( + mockClient.get(testUri, headers: anyNamed('headers')), + ).thenAnswer((_) => Future.value(Response('{"ok": true}', 200))); + + // Act + final provider = WgerBaseProvider(testAuthProvider, mockClient); + final result = await provider.fetch(testUri); + + // Assert + expect(result, isA()); + expect(result['ok'], isTrue); + verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(1); + }); +} diff --git a/test/providers/base_provider.mocks.dart b/test/providers/base_provider.mocks.dart new file mode 100644 index 00000000..5111102b --- /dev/null +++ b/test/providers/base_provider.mocks.dart @@ -0,0 +1,218 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in wger/test/providers/base_provider.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; +import 'dart:convert' as _i4; +import 'dart:typed_data' as _i6; + +import 'package:http/http.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i5; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member + +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeStreamedResponse_1 extends _i1.SmartFake implements _i2.StreamedResponse { + _FakeStreamedResponse_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [Client]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockClient extends _i1.Mock implements _i2.Client { + MockClient() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.Response> head(Uri? url, {Map? headers}) => + (super.noSuchMethod( + Invocation.method(#head, [url], {#headers: headers}), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method(#head, [url], {#headers: headers}), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> get(Uri? url, {Map? headers}) => + (super.noSuchMethod( + Invocation.method(#get, [url], {#headers: headers}), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method(#get, [url], {#headers: headers}), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> post( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method( + #post, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> put( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method( + #put, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> patch( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> delete( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + returnValue: _i3.Future<_i2.Response>.value( + _FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + {#headers: headers, #body: body, #encoding: encoding}, + ), + ), + ), + ) + as _i3.Future<_i2.Response>); + + @override + _i3.Future read(Uri? url, {Map? headers}) => + (super.noSuchMethod( + Invocation.method(#read, [url], {#headers: headers}), + returnValue: _i3.Future.value( + _i5.dummyValue( + this, + Invocation.method(#read, [url], {#headers: headers}), + ), + ), + ) + as _i3.Future); + + @override + _i3.Future<_i6.Uint8List> readBytes( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method(#readBytes, [url], {#headers: headers}), + returnValue: _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + ) + as _i3.Future<_i6.Uint8List>); + + @override + _i3.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => + (super.noSuchMethod( + Invocation.method(#send, [request]), + returnValue: _i3.Future<_i2.StreamedResponse>.value( + _FakeStreamedResponse_1( + this, + Invocation.method(#send, [request]), + ), + ), + ) + as _i3.Future<_i2.StreamedResponse>); + + @override + void close() => super.noSuchMethod( + Invocation.method(#close, []), + returnValueForMissingStub: null, + ); +} diff --git a/test/routine/gym_mode/gym_mode_test.mocks.dart b/test/routine/gym_mode/gym_mode_test.mocks.dart index 6b3c882c..81002948 100644 --- a/test/routine/gym_mode/gym_mode_test.mocks.dart +++ b/test/routine/gym_mode/gym_mode_test.mocks.dart @@ -201,9 +201,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i20.Future fetch(Uri? uri) => + _i20.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i20.Future.value(), ) as _i20.Future); diff --git a/test/routine/routine_screen_test.mocks.dart b/test/routine/routine_screen_test.mocks.dart index 99fe1643..41f40bb3 100644 --- a/test/routine/routine_screen_test.mocks.dart +++ b/test/routine/routine_screen_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/routine/routines_provider_test.mocks.dart b/test/routine/routines_provider_test.mocks.dart index 06326506..f9125d59 100644 --- a/test/routine/routines_provider_test.mocks.dart +++ b/test/routine/routines_provider_test.mocks.dart @@ -151,9 +151,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i11.Future fetch(Uri? uri) => + _i11.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i11.Future.value(), ) as _i11.Future); diff --git a/test/routine/routines_screen_test.mocks.dart b/test/routine/routines_screen_test.mocks.dart index 2607f048..ea1014f7 100644 --- a/test/routine/routines_screen_test.mocks.dart +++ b/test/routine/routines_screen_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/user/provider_test.mocks.dart b/test/user/provider_test.mocks.dart index 08bbf636..ecf42fc8 100644 --- a/test/user/provider_test.mocks.dart +++ b/test/user/provider_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/weight/weight_provider_test.mocks.dart b/test/weight/weight_provider_test.mocks.dart index 4cd18bb5..5fc97437 100644 --- a/test/weight/weight_provider_test.mocks.dart +++ b/test/weight/weight_provider_test.mocks.dart @@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 500), + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), returnValue: _i5.Future.value(), ) as _i5.Future); From 6620a83baa09e0b3389205e99252f4e80404efb6 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Wed, 14 Jan 2026 14:59:07 +0100 Subject: [PATCH 17/25] Improve html error detection In case the header is not correctly set, try to detect html error messages from the content itself --- lib/core/exceptions/http_exception.dart | 5 ++-- lib/helpers/errors.dart | 4 +-- lib/providers/auth.dart | 6 ++--- test/core/http_exception_test.dart | 34 +++++++++++++++++++++++-- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/lib/core/exceptions/http_exception.dart b/lib/core/exceptions/http_exception.dart index 948b8975..73cbc24c 100644 --- a/lib/core/exceptions/http_exception.dart +++ b/lib/core/exceptions/http_exception.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020 - 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -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('. - * 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( diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index edb73a8d..3f2e26fa 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020 - 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(), diff --git a/test/core/http_exception_test.dart b/test/core/http_exception_test.dart index 26b7a754..30ae455c 100644 --- a/test/core/http_exception_test.dart +++ b/test/core/http_exception_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020 - 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -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 = 'Error'; 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 = 'Error'; + final resp = http.Response( + body, + 500, + headers: {HttpHeaders.contentTypeHeader: 'text/foo; charset=utf-8'}, + ); + + // Act + final ex = WgerHttpException(resp); + + // Assert expect(ex.type, ErrorType.html); expect(ex.htmlError, body); }); test('fromMap sets errors and type', () { + // Arrange final map = {'field': 'value'}; + + // Act final ex = WgerHttpException.fromMap(map); + // Assert expect(ex.type, ErrorType.json); expect(ex.errors, map); }); From 136607db2534a99013cb5f249c6291cf742649ee Mon Sep 17 00:00:00 2001 From: Benjamin Voisin Date: Mon, 5 Jan 2026 23:03:54 +0100 Subject: [PATCH 18/25] Translated using Weblate (French) Currently translated at 100.0% (369 of 369 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/fr/ --- lib/l10n/app_fr.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 429c9e54..8a5cfc2c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -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" }, From 7c0f47f548ad2ed629bfdc32b047fba623d49a1d Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Wed, 14 Jan 2026 19:23:46 +0100 Subject: [PATCH 19/25] Bump dependencies in Gemfile.lock --- Gemfile.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1d7388fd..313cae7f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,8 +8,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1198.0) - aws-sdk-core (3.240.0) + 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,11 +17,11 @@ 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.208.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) @@ -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) @@ -171,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) @@ -180,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) From 5ef7671fabe7bce2969037c719f9a4b37002632d Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Wed, 14 Jan 2026 21:53:59 +0100 Subject: [PATCH 20/25] Bump version of flatpak-flutter --- .github/workflows/build-linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index b24ac7f3..84e01dd5 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -62,7 +62,7 @@ jobs: - name: Bump version and update manifest run: | - git clone https://github.com/TheAppgineer/flatpak-flutter.git --branch 0.10.0 ../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 From 1146c3902f4223e8f2c295af8a6276a465499716 Mon Sep 17 00:00:00 2001 From: Github-Actions Date: Wed, 14 Jan 2026 20:58:11 +0000 Subject: [PATCH 21/25] Bump version to 1.9.5 --- flatpak/de.wger.flutter.metainfo.xml | 6 ++++++ pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/flatpak/de.wger.flutter.metainfo.xml b/flatpak/de.wger.flutter.metainfo.xml index 8560be10..9b0daf14 100755 --- a/flatpak/de.wger.flutter.metainfo.xml +++ b/flatpak/de.wger.flutter.metainfo.xml @@ -84,6 +84,12 @@ + + +

Bug fixes and improvements.

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

Bug fixes and improvements.

diff --git a/pubspec.yaml b/pubspec.yaml index 14a8bd32..9738a313 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # - the version number is taken from the git tag vX.Y.Z # - the build number is computed by reading the last one from the play store # and increasing by one -version: 1.9.4+140 +version: 1.9.5+150 environment: sdk: '>=3.8.0 <4.0.0' From 53dcbd8c6c6343e31f7ddf519d50402615cd4d86 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Fri, 16 Jan 2026 15:26:38 +0100 Subject: [PATCH 22/25] Don't mark nullable fields as "late" These will be null and we avoid LateInitializationError errors See #1079 --- lib/models/workouts/log.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/models/workouts/log.dart b/lib/models/workouts/log.dart index 52b5c9f6..495ff7d3 100644 --- a/lib/models/workouts/log.dart +++ b/lib/models/workouts/log.dart @@ -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; From a249292afc7726b1b17c9e1d91792c5c56622483 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Fri, 16 Jan 2026 15:27:04 +0100 Subject: [PATCH 23/25] Don't show the session time if it's not set --- lib/widgets/dashboard/calendar.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/widgets/dashboard/calendar.dart b/lib/widgets/dashboard/calendar.dart index e7175fd1..3fa378ee 100644 --- a/lib/widgets/dashboard/calendar.dart +++ b/lib/widgets/dashboard/calendar.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -141,7 +141,9 @@ class _DashboardCalendarWidgetState extends State _events[date] = []; } var time = ''; - time = '(${timeToString(session.timeStart)} - ${timeToString(session.timeEnd)})'; + if (session.timeStart != null && session.timeEnd != null) { + time = '(${timeToString(session.timeStart)} - ${timeToString(session.timeEnd)})'; + } // Add events to lists _events[date]?.add( From 46fdf1efc7c5a011267fd9c72cb32b14b33a64cb Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Fri, 16 Jan 2026 15:28:09 +0100 Subject: [PATCH 24/25] Recreate generated files --- test/core/settings_test.mocks.dart | 2 +- test/exercises/contribute_exercise_image_test.mocks.dart | 2 +- test/gallery/gallery_form_test.mocks.dart | 2 +- test/gallery/gallery_screen_test.mocks.dart | 2 +- test/measurements/measurement_provider_test.mocks.dart | 2 +- test/nutrition/nutritional_plan_screen_test.mocks.dart | 2 +- test/nutrition/nutritional_plans_screen_test.mocks.dart | 2 +- test/routine/gym_mode/gym_mode_test.mocks.dart | 2 +- test/routine/routine_screen_test.mocks.dart | 2 +- test/routine/routines_provider_test.mocks.dart | 2 +- test/routine/routines_screen_test.mocks.dart | 2 +- test/user/provider_test.mocks.dart | 2 +- test/weight/weight_provider_test.mocks.dart | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/test/core/settings_test.mocks.dart b/test/core/settings_test.mocks.dart index 699e32d1..4e060322 100644 --- a/test/core/settings_test.mocks.dart +++ b/test/core/settings_test.mocks.dart @@ -1103,7 +1103,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { _i18.Future fetch( Uri? uri, { int? maxRetries = 3, - Duration? initialDelay = const Duration(milliseconds: 500), + Duration? initialDelay = const Duration(milliseconds: 250), }) => (super.noSuchMethod( Invocation.method( diff --git a/test/exercises/contribute_exercise_image_test.mocks.dart b/test/exercises/contribute_exercise_image_test.mocks.dart index b477e9d3..c20d1d75 100644 --- a/test/exercises/contribute_exercise_image_test.mocks.dart +++ b/test/exercises/contribute_exercise_image_test.mocks.dart @@ -405,7 +405,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { _i14.Future fetch( Uri? uri, { int? maxRetries = 3, - Duration? initialDelay = const Duration(milliseconds: 500), + Duration? initialDelay = const Duration(milliseconds: 250), }) => (super.noSuchMethod( Invocation.method( diff --git a/test/gallery/gallery_form_test.mocks.dart b/test/gallery/gallery_form_test.mocks.dart index 3ec16dfa..ad71121f 100644 --- a/test/gallery/gallery_form_test.mocks.dart +++ b/test/gallery/gallery_form_test.mocks.dart @@ -178,7 +178,7 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { _i6.Future fetch( Uri? uri, { int? maxRetries = 3, - Duration? initialDelay = const Duration(milliseconds: 500), + Duration? initialDelay = const Duration(milliseconds: 250), }) => (super.noSuchMethod( Invocation.method( diff --git a/test/gallery/gallery_screen_test.mocks.dart b/test/gallery/gallery_screen_test.mocks.dart index c9fe9e74..7ea2e32f 100644 --- a/test/gallery/gallery_screen_test.mocks.dart +++ b/test/gallery/gallery_screen_test.mocks.dart @@ -178,7 +178,7 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { _i6.Future fetch( Uri? uri, { int? maxRetries = 3, - Duration? initialDelay = const Duration(milliseconds: 500), + Duration? initialDelay = const Duration(milliseconds: 250), }) => (super.noSuchMethod( Invocation.method( diff --git a/test/measurements/measurement_provider_test.mocks.dart b/test/measurements/measurement_provider_test.mocks.dart index 96f42b39..7c766730 100644 --- a/test/measurements/measurement_provider_test.mocks.dart +++ b/test/measurements/measurement_provider_test.mocks.dart @@ -115,7 +115,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { _i5.Future fetch( Uri? uri, { int? maxRetries = 3, - Duration? initialDelay = const Duration(milliseconds: 500), + Duration? initialDelay = const Duration(milliseconds: 250), }) => (super.noSuchMethod( Invocation.method( diff --git a/test/nutrition/nutritional_plan_screen_test.mocks.dart b/test/nutrition/nutritional_plan_screen_test.mocks.dart index 3ee7049a..9b7d92ee 100644 --- a/test/nutrition/nutritional_plan_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plan_screen_test.mocks.dart @@ -125,7 +125,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { _i5.Future fetch( Uri? uri, { int? maxRetries = 3, - Duration? initialDelay = const Duration(milliseconds: 500), + Duration? initialDelay = const Duration(milliseconds: 250), }) => (super.noSuchMethod( Invocation.method( diff --git a/test/nutrition/nutritional_plans_screen_test.mocks.dart b/test/nutrition/nutritional_plans_screen_test.mocks.dart index 5c9dae55..1d9f0fb0 100644 --- a/test/nutrition/nutritional_plans_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plans_screen_test.mocks.dart @@ -360,7 +360,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i8.WgerBaseProvider { _i5.Future fetch( Uri? uri, { int? maxRetries = 3, - Duration? initialDelay = const Duration(milliseconds: 500), + Duration? initialDelay = const Duration(milliseconds: 250), }) => (super.noSuchMethod( Invocation.method( diff --git a/test/routine/gym_mode/gym_mode_test.mocks.dart b/test/routine/gym_mode/gym_mode_test.mocks.dart index 81002948..9541140d 100644 --- a/test/routine/gym_mode/gym_mode_test.mocks.dart +++ b/test/routine/gym_mode/gym_mode_test.mocks.dart @@ -204,7 +204,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { _i20.Future fetch( Uri? uri, { int? maxRetries = 3, - Duration? initialDelay = const Duration(milliseconds: 500), + Duration? initialDelay = const Duration(milliseconds: 250), }) => (super.noSuchMethod( Invocation.method( diff --git a/test/routine/routine_screen_test.mocks.dart b/test/routine/routine_screen_test.mocks.dart index 41f40bb3..7c623651 100644 --- a/test/routine/routine_screen_test.mocks.dart +++ b/test/routine/routine_screen_test.mocks.dart @@ -115,7 +115,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { _i5.Future fetch( Uri? uri, { int? maxRetries = 3, - Duration? initialDelay = const Duration(milliseconds: 500), + Duration? initialDelay = const Duration(milliseconds: 250), }) => (super.noSuchMethod( Invocation.method( diff --git a/test/routine/routines_provider_test.mocks.dart b/test/routine/routines_provider_test.mocks.dart index f9125d59..3fc8c4ee 100644 --- a/test/routine/routines_provider_test.mocks.dart +++ b/test/routine/routines_provider_test.mocks.dart @@ -154,7 +154,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { _i11.Future fetch( Uri? uri, { int? maxRetries = 3, - Duration? initialDelay = const Duration(milliseconds: 500), + Duration? initialDelay = const Duration(milliseconds: 250), }) => (super.noSuchMethod( Invocation.method( diff --git a/test/routine/routines_screen_test.mocks.dart b/test/routine/routines_screen_test.mocks.dart index ea1014f7..95d3ca97 100644 --- a/test/routine/routines_screen_test.mocks.dart +++ b/test/routine/routines_screen_test.mocks.dart @@ -115,7 +115,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { _i5.Future fetch( Uri? uri, { int? maxRetries = 3, - Duration? initialDelay = const Duration(milliseconds: 500), + Duration? initialDelay = const Duration(milliseconds: 250), }) => (super.noSuchMethod( Invocation.method( diff --git a/test/user/provider_test.mocks.dart b/test/user/provider_test.mocks.dart index ecf42fc8..4e7436d0 100644 --- a/test/user/provider_test.mocks.dart +++ b/test/user/provider_test.mocks.dart @@ -115,7 +115,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { _i5.Future fetch( Uri? uri, { int? maxRetries = 3, - Duration? initialDelay = const Duration(milliseconds: 500), + Duration? initialDelay = const Duration(milliseconds: 250), }) => (super.noSuchMethod( Invocation.method( diff --git a/test/weight/weight_provider_test.mocks.dart b/test/weight/weight_provider_test.mocks.dart index 5fc97437..be3d3857 100644 --- a/test/weight/weight_provider_test.mocks.dart +++ b/test/weight/weight_provider_test.mocks.dart @@ -115,7 +115,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { _i5.Future fetch( Uri? uri, { int? maxRetries = 3, - Duration? initialDelay = const Duration(milliseconds: 500), + Duration? initialDelay = const Duration(milliseconds: 250), }) => (super.noSuchMethod( Invocation.method( From 381d28d0441c29c40f3493d168edb50c655ddbd9 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Fri, 16 Jan 2026 15:40:27 +0100 Subject: [PATCH 25/25] Slightly refactor the fetch session data part in the calendar. In general, it doesn't make much sense that the sessions are the only data points that are loaded live every time, all the others are simply read from the respective providers. Hopefully all this can be removed when (if) we move to using a local sqlite db with powersync. --- lib/providers/routines.dart | 2 +- lib/widgets/dashboard/calendar.dart | 42 +++++++++++++---------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/lib/providers/routines.dart b/lib/providers/routines.dart index 56d163dc..67210c40 100644 --- a/lib/providers/routines.dart +++ b/lib/providers/routines.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/lib/widgets/dashboard/calendar.dart b/lib/widgets/dashboard/calendar.dart index 3fa378ee..2809734c 100644 --- a/lib/widgets/dashboard/calendar.dart +++ b/lib/widgets/dashboard/calendar.dart @@ -134,35 +134,29 @@ class _DashboardCalendarWidgetState extends State // Process workout sessions final routinesProvider = context.read(); - await routinesProvider.fetchSessionData().then((sessions) { - for (final session in sessions) { - final date = DateFormatLists.format(session.date); - if (!_events.containsKey(date)) { - _events[date] = []; - } - var time = ''; - if (session.timeStart != null && session.timeEnd != null) { - time = '(${timeToString(session.timeStart)} - ${timeToString(session.timeEnd)})'; - } - - // Add events to lists - _events[date]?.add( - Event( - EventType.session, - '${i18n.impression}: ${session.impressionAsString(context)} $time', - ), - ); - } - }); + final sessions = await routinesProvider.fetchSessionData(); if (!mounted) { return; } + for (final session in sessions) { + final date = DateFormatLists.format(session.date); + _events.putIfAbsent(date, () => []); + + final time = (session.timeStart != null && session.timeEnd != null) + ? '(${timeToString(session.timeStart)} - ${timeToString(session.timeEnd)})' + : ''; + + _events[date]?.add( + Event( + EventType.session, + '${i18n.impression}: ${session.impressionAsString(context)}${time.isNotEmpty ? ' $time' : ''}', + ), + ); + } + // Process nutritional plans - final NutritionPlansProvider nutritionProvider = Provider.of( - context, - listen: false, - ); + final nutritionProvider = context.read(); for (final plan in nutritionProvider.items) { for (final entry in plan.logEntriesValues.entries) { final date = DateFormatLists.format(entry.key);