From 13421c547124ae46b4a3c86d84a84cfa4743fafc Mon Sep 17 00:00:00 2001 From: DhruvSingh19 Date: Wed, 3 Dec 2025 23:44:01 +0530 Subject: [PATCH 01/54] Fix: Dead-Screen Issue is fixed --- ios/Flutter/ephemeral/flutter_lldb_helper.py | 32 ++++++++++++++++++++ ios/Flutter/ephemeral/flutter_lldbinit | 5 +++ lib/screens/measurement_entries_screen.dart | 25 ++++++++++++--- 3 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 ios/Flutter/ephemeral/flutter_lldb_helper.py create mode 100644 ios/Flutter/ephemeral/flutter_lldbinit diff --git a/ios/Flutter/ephemeral/flutter_lldb_helper.py b/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 00000000..a88caf99 --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/ios/Flutter/ephemeral/flutter_lldbinit b/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 00000000..e3ba6fbe --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/lib/screens/measurement_entries_screen.dart b/lib/screens/measurement_entries_screen.dart index 688bd868..aa693b8c 100644 --- a/lib/screens/measurement_entries_screen.dart +++ b/lib/screens/measurement_entries_screen.dart @@ -24,6 +24,7 @@ import 'package:wger/providers/measurement.dart'; import 'package:wger/screens/form_screen.dart'; import 'package:wger/widgets/measurements/entries.dart'; import 'package:wger/widgets/measurements/forms.dart'; +import '../models/measurements/measurement_category.dart'; enum MeasurementOptions { edit, @@ -38,7 +39,20 @@ class MeasurementEntriesScreen extends StatelessWidget { @override Widget build(BuildContext context) { final categoryId = ModalRoute.of(context)!.settings.arguments as int; - final category = Provider.of(context).findCategoryById(categoryId); + final provider = Provider.of(context); + MeasurementCategory? category; + + try { + category = provider.findCategoryById(categoryId); + } catch (e) { + // Category deleted → prevent red screen + Future.microtask(() { + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + }); + return const SizedBox(); // Return empty widget until pop happens + } return Scaffold( appBar: AppBar( @@ -65,7 +79,7 @@ class MeasurementEntriesScreen extends StatelessWidget { builder: (BuildContext contextDialog) { return AlertDialog( content: Text( - AppLocalizations.of(context).confirmDelete(category.name), + AppLocalizations.of(context).confirmDelete(category!.name), ), actions: [ TextButton( @@ -84,11 +98,12 @@ class MeasurementEntriesScreen extends StatelessWidget { Provider.of( context, listen: false, - ).deleteCategory(category.id!); - + ).deleteCategory(category!.id!); // Close the popup Navigator.of(contextDialog).pop(); + Navigator.of(context).pop(); // Exit detail screen + // and inform the user ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -138,7 +153,7 @@ class MeasurementEntriesScreen extends StatelessWidget { body: WidescreenWrapper( child: SingleChildScrollView( child: Consumer( - builder: (context, provider, child) => EntriesList(category), + builder: (context, provider, child) => EntriesList(category!), ), ), ), From bc6a8a7273eacf7a226039249af95a5dab224786 Mon Sep 17 00:00:00 2001 From: DhruvSingh19 Date: Thu, 4 Dec 2025 09:46:17 +0530 Subject: [PATCH 02/54] Implemented Copilot's suggestions. --- lib/screens/measurement_entries_screen.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/screens/measurement_entries_screen.dart b/lib/screens/measurement_entries_screen.dart index aa693b8c..550a3238 100644 --- a/lib/screens/measurement_entries_screen.dart +++ b/lib/screens/measurement_entries_screen.dart @@ -19,12 +19,13 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:wger/core/wide_screen_wrapper.dart'; +import 'package:wger/exceptions/no_such_entry_exception.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/providers/measurement.dart'; import 'package:wger/screens/form_screen.dart'; import 'package:wger/widgets/measurements/entries.dart'; import 'package:wger/widgets/measurements/forms.dart'; -import '../models/measurements/measurement_category.dart'; enum MeasurementOptions { edit, @@ -44,10 +45,9 @@ class MeasurementEntriesScreen extends StatelessWidget { try { category = provider.findCategoryById(categoryId); - } catch (e) { - // Category deleted → prevent red screen + } on NoSuchEntryException { Future.microtask(() { - if (Navigator.of(context).canPop()) { + if (context.mounted && Navigator.of(context).canPop()) { Navigator.of(context).pop(); } }); From 7a97b87106fcd45fe68f127ecc4bc03942d3540f Mon Sep 17 00:00:00 2001 From: Diego Menezes Date: Thu, 4 Dec 2025 21:05:53 +0100 Subject: [PATCH 03/54] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (369 of 369 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/pt_BR/ --- lib/l10n/app_pt_BR.arb | 204 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 186 insertions(+), 18 deletions(-) diff --git a/lib/l10n/app_pt_BR.arb b/lib/l10n/app_pt_BR.arb index 85ff9ccf..a21afbd4 100644 --- a/lib/l10n/app_pt_BR.arb +++ b/lib/l10n/app_pt_BR.arb @@ -115,7 +115,7 @@ "@close": { "description": "Translation for close" }, - "successfullyDeleted": "Excluído", + "successfullyDeleted": "Excluído com êxito", "@successfullyDeleted": { "description": "Message when an item was successfully deleted" }, @@ -125,13 +125,13 @@ "@goToToday": { "description": "Label on button to jump back to 'today' in the calendar widget" }, - "set": "Definir", + "set": "Série", "@set": { "description": "A set in a workout plan" }, "noMeasurementEntries": "Você não tem entradas de medição", "@noMeasurementEntries": {}, - "newSet": "Novo conjunto", + "newSet": "Nova séries", "@newSet": { "description": "Header when adding a new set to a workout day" }, @@ -189,7 +189,7 @@ "@pause": { "description": "Noun, not an imperative! Label used for the pause when using the gym mode" }, - "success": "Sucesso", + "success": "Êxito", "@success": { "description": "Message when an action completed successfully, usually used as a heading" }, @@ -213,7 +213,7 @@ "@newEntry": { "description": "Title when adding a new entry such as a weight or log entry" }, - "addSet": "Adicionar set", + "addSet": "Adicionar séries", "@addSet": { "description": "Label for the button that adds a set (to a workout day)" }, @@ -271,7 +271,7 @@ "@loadingText": { "description": "Text to show when entries are being loaded in the background: Loading..." }, - "selectExercises": "Se quiser fazer um superset você pode procurar vários exercícios, eles estarão agrupados", + "selectExercises": "Se quiser fazer um superséries você pode procurar vários exercícios, eles estarão agrupados", "@selectExercises": {}, "nutritionalDiary": "Diário nutricional", "@nutritionalDiary": {}, @@ -953,7 +953,7 @@ "@noRoutines": {}, "restTime": "Tempo de descanso", "@restTime": {}, - "sets": "Conjuntos", + "sets": "Séries", "@sets": { "description": "The number of sets to be done for one exercise" }, @@ -967,7 +967,7 @@ } } }, - "supersetNr": "Superset {nr}", + "supersetNr": "Supersérie {nr}", "@supersetNr": { "description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Superset Nr. xy'.", "type": "text", @@ -981,7 +981,7 @@ "@restDay": {}, "isRestDay": "É dia de descanso", "@isRestDay": {}, - "isRestDayHelp": "Por favor, note que todos os conjuntos e exercícios serão removidos quando marcar um dia como um dia de descanso.", + "isRestDayHelp": "Por favor, note que todos as séries e exercícios serão removidos quando marcar um dia como um dia de descanso.", "@isRestDayHelp": {}, "needsLogsToAdvance": "Precisa de logs para avançar", "@needsLogsToAdvance": {}, @@ -1005,7 +1005,7 @@ "@toAddMealsToThePlanGoToNutritionalPlanDetails": { "description": "Message shown to guide users to the nutritional plan details page to add meals" }, - "errorInfoDescription": "Algo de errado aconteceu. Você pode nos ajudar a concertar esse problema reportando o problema no Github", + "errorInfoDescription": "Algo de errado aconteceu. Você pode nos ajudar a concertar esse problema reportando o problema no Github.", "@errorInfoDescription": {}, "errorInfoDescription2": "Você pode continuar usando o applicativo, mas algumas funcionalidades não estarão disponíveis.", "@errorInfoDescription2": {}, @@ -1021,7 +1021,7 @@ "@min": {}, "max": "Max", "@max": {}, - "aboutWhySupportTitle": "Open Source & free to use ❤️", + "aboutWhySupportTitle": "Código aberto e de uso gratuito ❤️", "@aboutWhySupportTitle": {}, "aboutContributeTitle": "Contribua", "@aboutContributeTitle": {}, @@ -1043,13 +1043,13 @@ "@fitInWeek": {}, "fitInWeekHelp": "Se ligado, os dias vão se repetir semanalmente, caso contrário os dias seguirão sequencialmente se considerar o começo de uma nova semana.", "@fitInWeekHelp": {}, - "addSuperset": "Adicionar superset", + "addSuperset": "Adicionar superséries", "@addSuperset": {}, "setHasProgression": "Treino tem prograssão", "@setHasProgression": {}, - "setHasProgressionWarning": "Observe que, no momento, não é possível editar todas as configurações de um conjunto no aplicativo móvel nem configurar a progressão automática. Por enquanto, use o aplicativo web.", + "setHasProgressionWarning": "Observe que, no momento, não é possível editar todas as configurações de um séries no aplicativo móvel nem configurar a progressão automática. Por enquanto, use o aplicativo web.", "@setHasProgressionWarning": {}, - "setHasNoExercises": "Este treino ainda não tem exercícios!", + "setHasNoExercises": "Este séries ainda não tem exercícios!", "@setHasNoExercises": {}, "simpleMode": "Modo simples", "@simpleMode": {}, @@ -1057,7 +1057,7 @@ "@simpleModeHelp": {}, "progressionRules": "Este exercício tem regras de progressão e não pode ser editado no aplicativo móvel. Use o aplicativo web para editá-lo.", "@progressionRules": {}, - "resistance_band": "Resistance band", + "resistance_band": "Banda de resistência", "@resistance_band": { "description": "Generated entry for translation for server strings" }, @@ -1077,8 +1077,176 @@ "@startDate": {}, "dayTypeCustom": "Personalizado", "@dayTypeCustom": {}, - "dayTypeHiit": "Treino de alta intensidade", + "dayTypeHiit": "Treinamento intervalado de alta intensidade", "@dayTypeHiit": {}, - "dayTypeTabata": "Tabata", - "@dayTypeTabata": {} + "dayTypeTabata": "Método Tabata", + "@dayTypeTabata": {}, + "impressionGood": "Boa", + "@impressionGood": {}, + "impressionNeutral": "Neutra", + "@impressionNeutral": {}, + "impressionBad": "Ruim", + "@impressionBad": {}, + "gymModeShowExercises": "Mostrar páginas de visão geral dos exercícios", + "@gymModeShowExercises": {}, + "gymModeShowTimer": "Mostrar cronômetro entre séries", + "@gymModeShowTimer": {}, + "gymModeTimerType": "Tipo de temporizador", + "@gymModeTimerType": {}, + "gymModeTimerTypeHelText": "Se uma série tiver tempo de pausa, sempre será usada uma contagem regressiva.", + "@gymModeTimerTypeHelText": {}, + "countdown": "Contagem regressiva", + "@countdown": {}, + "stopwatch": "cronômetro", + "@stopwatch": {}, + "gymModeDefaultCountdownTime": "Tempo de contagem regressiva padrão, em segundos", + "@gymModeDefaultCountdownTime": {}, + "gymModeNotifyOnCountdownFinish": "Notificar no final da contagem regressiva", + "@gymModeNotifyOnCountdownFinish": {}, + "duration": "Duração", + "@duration": {}, + "durationHoursMinutes": "{hours}h {minutes}m", + "@durationHoursMinutes": { + "description": "A duration, in hours and minutes", + "type": "text", + "placeholders": { + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + } + } + }, + "volume": "Volume", + "@volume": { + "description": "The volume of a workout or set, i.e. weight x reps" + }, + "workoutCompleted": "Treino concluído", + "@workoutCompleted": {}, + "dayTypeEnom": "Cada minuto a minuto", + "@dayTypeEnom": {}, + "dayTypeAmrap": "Tantas rodadas quanto possível", + "@dayTypeAmrap": {}, + "dayTypeEdt": "Treinamento de densidade crescente", + "@dayTypeEdt": {}, + "dayTypeRft": "Rodadas para ganhar tempo", + "@dayTypeRft": {}, + "dayTypeAfap": "O mais rápido possível", + "@dayTypeAfap": {}, + "slotEntryTypeNormal": "Normal", + "@slotEntryTypeNormal": {}, + "slotEntryTypePartial": "Parcial", + "@slotEntryTypePartial": {}, + "slotEntryTypeForced": "Forçado", + "@slotEntryTypeForced": {}, + "slotEntryTypeTut": "Tempo Sob Tensão", + "@slotEntryTypeTut": {}, + "slotEntryTypeIso": "Fixação isométrica", + "@slotEntryTypeIso": {}, + "slotEntryTypeJump": "Pular", + "@slotEntryTypeJump": {}, + "applicationLogs": "Registros de aplicativos", + "@applicationLogs": {}, + "openEnded": "Aberto", + "@openEnded": { + "description": "When a nutrition plan has no pre-defined end date" + }, + "overview": "visão global", + "@overview": {}, + "formMinMaxValues": "Insira um valor entre {min} e {max}", + "@formMinMaxValues": { + "description": "Error message when the user needs to enter a value between min and max", + "type": "text", + "placeholders": { + "min": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + "identicalExercisePleaseDiscard": "Se você notar um exercício idêntico ao que você está adicionando, descarte o rascunho e edite o exercício.", + "@identicalExercisePleaseDiscard": {}, + "checkInformationBeforeSubmitting": "Verifique se as informações inseridas estão corretas antes de enviar o exercício", + "@checkInformationBeforeSubmitting": {}, + "imageDetailsTitle": "Detalhes da imagem", + "@imageDetailsTitle": { + "description": "Title for image details form" + }, + "imageDetailsLicenseTitle": "Titulo", + "@imageDetailsLicenseTitle": { + "description": "Label for image title field" + }, + "imageDetailsLicenseTitleHint": "Insira o título da imagem", + "@imageDetailsLicenseTitleHint": { + "description": "Hint text for image title field" + }, + "imageDetailsSourceLink": "Link para o site de origem", + "@imageDetailsSourceLink": { + "description": "Label for source link field" + }, + "author": "Autor(s)", + "@author": {}, + "authorHint": "Digite o nome do autor", + "@authorHint": { + "description": "Hint text for author field" + }, + "imageDetailsAuthorLink": "Link para o site ou perfil do autor", + "@imageDetailsAuthorLink": { + "description": "Label for author link field" + }, + "imageDetailsDerivativeSource": "Link para a fonte original, se este for um trabalho derivado", + "@imageDetailsDerivativeSource": { + "description": "Label for derivative source field" + }, + "imageDetailsDerivativeHelp": "Um trabalho derivado é baseado em um trabalho anterior, mas contém conteúdo novo e criativo suficiente para ter direito aos seus próprios direitos autorais.", + "@imageDetailsDerivativeHelp": { + "description": "Helper text explaining derivative works" + }, + "imageDetailsImageType": "Tipo de imagem", + "@imageDetailsImageType": { + "description": "Label for image type selector" + }, + "imageDetailsLicenseNotice": "Ao enviar esta imagem, você concorda em liberá-la sob CC-BY-SA-4. A imagem deve ser de sua autoria ou o autor deve tê-la divulgado sob uma licença compatível com ela.", + "@imageDetailsLicenseNotice": {}, + "imageDetailsLicenseNoticeLinkToLicense": "Consulte o texto da licença.", + "@imageDetailsLicenseNoticeLinkToLicense": {}, + "imageFormatNotSupported": "{imageFormat} não compatível", + "@imageFormatNotSupported": { + "description": "Label shown on the error container when image format is not supported", + "type": "text", + "placeholders": { + "imageFormat": { + "type": "String" + } + } + }, + "imageFormatNotSupportedDetail": "Imagens {imageFormat} ainda não são suportadas.", + "@imageFormatNotSupportedDetail": { + "description": "Label shown on the image preview container when image format is not supported", + "type": "text", + "placeholders": { + "imageFormat": { + "type": "String" + } + } + }, + "add": "adicionar", + "@add": { + "description": "Add button text" + }, + "superset": "Supersérie", + "@superset": {}, + "enterTextInLanguage": "Por favor, insira o texto no idioma correto!", + "@enterTextInLanguage": {}, + "endWorkout": "Terminar treino", + "@endWorkout": { + "description": "Use the imperative, label on button to finish the current workout in gym mode" + }, + "slotEntryTypeMyo": "Myo", + "@slotEntryTypeMyo": {}, + "slotEntryTypeDropset": "Drop set", + "@slotEntryTypeDropset": {} } From f54c815bf7dae20bb05a8e05624734ffecf92f3c Mon Sep 17 00:00:00 2001 From: Diego Menezes Date: Sat, 6 Dec 2025 02:20:18 +0100 Subject: [PATCH 04/54] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (369 of 369 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/pt_BR/ --- lib/l10n/app_pt_BR.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_pt_BR.arb b/lib/l10n/app_pt_BR.arb index a21afbd4..2e3d2e26 100644 --- a/lib/l10n/app_pt_BR.arb +++ b/lib/l10n/app_pt_BR.arb @@ -115,7 +115,7 @@ "@close": { "description": "Translation for close" }, - "successfullyDeleted": "Excluído com êxito", + "successfullyDeleted": "Removido com êxito", "@successfullyDeleted": { "description": "Message when an item was successfully deleted" }, @@ -189,7 +189,7 @@ "@pause": { "description": "Noun, not an imperative! Label used for the pause when using the gym mode" }, - "success": "Êxito", + "success": "Bem-sucedido", "@success": { "description": "Message when an action completed successfully, usually used as a heading" }, From 4cc0aaf3001a47b0c4f942566ad24936f95fcc0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=93=D0=BE=D1=80?= =?UTF-8?q?=D0=BF=D0=B8=D0=BD=D1=96=D1=87?= Date: Mon, 8 Dec 2025 04:50:58 +0100 Subject: [PATCH 05/54] Translated using Weblate (Ukrainian) Currently translated at 100.0% (369 of 369 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/uk/ --- lib/l10n/app_uk.arb | 60 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 1d88c768..243d31f6 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1162,5 +1162,63 @@ "slotEntryTypeJump": "Стрибок", "@slotEntryTypeJump": {}, "endWorkout": "Закінчити тренування", - "@endWorkout": {} + "@endWorkout": {}, + "impressionGood": "Добре", + "@impressionGood": {}, + "impressionNeutral": "Нейтральний", + "@impressionNeutral": {}, + "impressionBad": "Погано", + "@impressionBad": {}, + "gymModeShowExercises": "Показати сторінки огляду вправ", + "@gymModeShowExercises": {}, + "gymModeShowTimer": "Показувати таймер між сетами", + "@gymModeShowTimer": {}, + "gymModeTimerType": "Тип таймера", + "@gymModeTimerType": {}, + "gymModeTimerTypeHelText": "Якщо сет має час паузи, завжди використовується зворотний відлік.", + "@gymModeTimerTypeHelText": {}, + "countdown": "Зворотний відлік", + "@countdown": {}, + "stopwatch": "Секундомір", + "@stopwatch": {}, + "gymModeDefaultCountdownTime": "Час зворотного відліку за замовчуванням, у секундах", + "@gymModeDefaultCountdownTime": {}, + "gymModeNotifyOnCountdownFinish": "Повідомити про закінчення зворотного відліку", + "@gymModeNotifyOnCountdownFinish": {}, + "duration": "Тривалість", + "@duration": {}, + "durationHoursMinutes": "{hours}г {minutes}хв", + "@durationHoursMinutes": { + "description": "A duration, in hours and minutes", + "type": "text", + "placeholders": { + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + } + } + }, + "volume": "Обсяг", + "@volume": { + "description": "The volume of a workout or set, i.e. weight x reps" + }, + "workoutCompleted": "Тренування завершено", + "@workoutCompleted": {}, + "formMinMaxValues": "Будь ласка, введіть значення від {min} до {max}", + "@formMinMaxValues": { + "description": "Error message when the user needs to enter a value between min and max", + "type": "text", + "placeholders": { + "min": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + "superset": "Суперсет", + "@superset": {} } From 24415da42af177f2adf8b41d5a935f4a9e85930c Mon Sep 17 00:00:00 2001 From: Elias Lang Date: Sun, 7 Dec 2025 20:06:47 +0100 Subject: [PATCH 06/54] Translated using Weblate (German) Currently translated at 100.0% (369 of 369 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/de/ --- lib/l10n/app_de.arb | 64 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f768dcd6..40a9062d 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -705,7 +705,7 @@ "@whatVariationsExist": {}, "previous": "Vorherige", "@previous": {}, - "next": "Nächste", + "next": "Weiter", "@next": {}, "swiss_ball": "Gymnastikball", "@swiss_ball": {}, @@ -1127,5 +1127,65 @@ "endWorkout": "Training beenden", "@endWorkout": {}, "dayTypeCustom": "personalisierte", - "@dayTypeCustom": {} + "@dayTypeCustom": {}, + "dayTypeTabata": "Tabata", + "@dayTypeTabata": {}, + "impressionGood": "Gut", + "@impressionGood": {}, + "impressionNeutral": "Neutral", + "@impressionNeutral": {}, + "impressionBad": "Schlecht", + "@impressionBad": {}, + "gymModeShowExercises": "Übersichtsseiten der Übungen anzeigen", + "@gymModeShowExercises": {}, + "gymModeShowTimer": "Zeige Timer zwischen den Sätzen", + "@gymModeShowTimer": {}, + "gymModeTimerType": "Timer-Typ", + "@gymModeTimerType": {}, + "gymModeTimerTypeHelText": "Wenn ein Satz eine Pausenzeit hat, wird immer ein Countdown verwendet.", + "@gymModeTimerTypeHelText": {}, + "countdown": "Countdown", + "@countdown": {}, + "stopwatch": "Stoppuhr", + "@stopwatch": {}, + "gymModeDefaultCountdownTime": "Standard-Countdown-Zeit in Sekunden", + "@gymModeDefaultCountdownTime": {}, + "gymModeNotifyOnCountdownFinish": "Benachrichtigung bei Ende des Countdowns", + "@gymModeNotifyOnCountdownFinish": {}, + "duration": "Dauer", + "@duration": {}, + "durationHoursMinutes": "{hours}h {minutes}m", + "@durationHoursMinutes": { + "description": "A duration, in hours and minutes", + "type": "text", + "placeholders": { + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + } + } + }, + "volume": "Volumen", + "@volume": { + "description": "The volume of a workout or set, i.e. weight x reps" + }, + "workoutCompleted": "Training abgeschlossen", + "@workoutCompleted": {}, + "formMinMaxValues": "Bitte geben Sie einen Wert zwischen {min} und {max}", + "@formMinMaxValues": { + "description": "Error message when the user needs to enter a value between min and max", + "type": "text", + "placeholders": { + "min": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + "superset": "Superset", + "@superset": {} } From f91fb026c4009bb44113461bf5c356457df44d42 Mon Sep 17 00:00:00 2001 From: Floris C Date: Fri, 12 Dec 2025 16:55:25 +0100 Subject: [PATCH 07/54] Translated using Weblate (Dutch) Currently translated at 58.5% (216 of 369 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/nl/ --- lib/l10n/app_nl.arb | 502 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 501 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 4e3b3012..6da9385a 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -194,5 +194,505 @@ "slotEntryTypeNormal": "Normaal", "@slotEntryTypeNormal": {}, "slotEntryTypeDropset": "Dropset", - "@slotEntryTypeDropset": {} + "@slotEntryTypeDropset": {}, + "slotEntryTypePartial": "Deels", + "@slotEntryTypePartial": {}, + "slotEntryTypeForced": "Verplicht", + "@slotEntryTypeForced": {}, + "slotEntryTypeTut": "Tijd onder spanning", + "@slotEntryTypeTut": {}, + "slotEntryTypeIso": "Isometrische houding", + "@slotEntryTypeIso": {}, + "slotEntryTypeJump": "Springen", + "@slotEntryTypeJump": {}, + "routines": "Routines", + "@routines": {}, + "newRoutine": "Nieuwe routine", + "@newRoutine": {}, + "noRoutines": "U heeft geen routines", + "@noRoutines": {}, + "restTime": "Rust tijd", + "@restTime": {}, + "sets": "Sets", + "@sets": { + "description": "The number of sets to be done for one exercise" + }, + "rir": "RiR", + "@rir": { + "description": "Shorthand for Repetitions In Reserve" + }, + "rirNotUsed": "Ongebruikt RiR", + "@rirNotUsed": { + "description": "Label used in RiR slider when the RiR value is not used/saved for the current setting or log" + }, + "useMetric": "Gebruik metrische eenheden voor lichaamsgewicht", + "@useMetric": {}, + "weightUnit": "Gewichtseenheid", + "@weightUnit": {}, + "repetitionUnit": "Herhalingseenheid", + "@repetitionUnit": {}, + "set": "Set", + "@set": { + "description": "A set in a workout plan" + }, + "dayDescriptionHelp": "Een beschrijving van wat er deze dag is gedaan (b.v. 'pull dag') of welk lichaamsdeel getraind is (b.v. 'borst en schouders')", + "@dayDescriptionHelp": {}, + "exerciseNr": "Oefening {nr}", + "@exerciseNr": { + "description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Set Nr. xy'.", + "type": "text", + "placeholders": { + "nr": { + "type": "String" + } + } + }, + "supersetNr": "Superset {nr}", + "@supersetNr": { + "description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Superset Nr. xy'.", + "type": "text", + "placeholders": { + "nr": { + "type": "String" + } + } + }, + "sameRepetitions": "Als je dezelfde herhalingen en gewichten gebruikt voor alle sets kan je één rij invullen. Voor 4 sets kan je bijvoorbeeld 10 invullen voor de herhalingen, dit wordt dan automatisch \"4 x 10\".", + "@sameRepetitions": {}, + "impressionGood": "Goed", + "@impressionGood": {}, + "impressionNeutral": "Neutraal", + "@impressionNeutral": {}, + "impressionBad": "Slecht", + "@impressionBad": {}, + "impression": "Impressie", + "@impression": { + "description": "General impression (e.g. for a workout session) such as good, bad, etc." + }, + "notes": "Notities", + "@notes": { + "description": "Personal notes, e.g. for a workout session" + }, + "workoutSession": "Workout sessie", + "@workoutSession": { + "description": "A (logged) workout session" + }, + "restDay": "Rust dag", + "@restDay": {}, + "isRestDay": "Is rust dag", + "@isRestDay": {}, + "isRestDayHelp": "Houd er rekening mee dat alle sets en oefeningen verwijderd worden als u deze dag markeert als rust dag.", + "@isRestDayHelp": {}, + "needsLogsToAdvance": "Vereist logs om door te gaan", + "@needsLogsToAdvance": {}, + "needsLogsToAdvanceHelp": "Selecteer als je wilt dat de routine alleen doorgaat naar de volgende dag als je voor de dag een workout hebt vastgelegd", + "@needsLogsToAdvanceHelp": {}, + "routineDays": "Dagen in routine", + "@routineDays": {}, + "resultingRoutine": "Resulterende routine", + "@resultingRoutine": {}, + "newDay": "Nieuwe dag", + "@newDay": {}, + "newSet": "Nieuwe set", + "@newSet": { + "description": "Header when adding a new set to a workout day" + }, + "selectExercises": "Als je een superset wilt doen kan je naar verschillende oefeningen zoeken, en kan je ze samen groeperen", + "@selectExercises": {}, + "gymMode": "Gym modus", + "@gymMode": { + "description": "Label when starting the gym mode" + }, + "gymModeShowExercises": "Toon oefeningsoverzicht paginas", + "@gymModeShowExercises": {}, + "gymModeShowTimer": "Toon timer tussen sets", + "@gymModeShowTimer": {}, + "gymModeTimerType": "Timer type", + "@gymModeTimerType": {}, + "gymModeTimerTypeHelText": "Als een set pauze tijd heeft, wordt altijd een afteller gebruikt.", + "@gymModeTimerTypeHelText": {}, + "countdown": "Afteller", + "@countdown": {}, + "stopwatch": "Stopwatch", + "@stopwatch": {}, + "gymModeDefaultCountdownTime": "Standaard afteltijd, in secondes", + "@gymModeDefaultCountdownTime": {}, + "gymModeNotifyOnCountdownFinish": "Notificeer op einde aftelling", + "@gymModeNotifyOnCountdownFinish": {}, + "duration": "Duur", + "@duration": {}, + "durationHoursMinutes": "{hours}u {minutes}m", + "@durationHoursMinutes": { + "description": "A duration, in hours and minutes", + "type": "text", + "placeholders": { + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + } + } + }, + "volume": "Volume", + "@volume": { + "description": "The volume of a workout or set, i.e. weight x reps" + }, + "workoutCompleted": "Workout voltooid", + "@workoutCompleted": {}, + "plateCalculator": "Platen", + "@plateCalculator": { + "description": "Label used for the plate calculator in the gym mode" + }, + "plateCalculatorNotDivisible": "Niet mogelijk om gewicht te bereiken met beschikbare platen", + "@plateCalculatorNotDivisible": { + "description": "Error message when the current weight is not reachable with plates (e.g. 33.1 kg)" + }, + "pause": "Pauzeer", + "@pause": { + "description": "Noun, not an imperative! Label used for the pause when using the gym mode" + }, + "jumpTo": "Ga naar", + "@jumpTo": { + "description": "Imperative. Label used in popup allowing the user to jump to a specific exercise while in the gym mode" + }, + "todaysWorkout": "Uw workout vandaag", + "@todaysWorkout": {}, + "logHelpEntries": "Als er op een enkele dag meerdere oefeningen met hetzelfde aantal herhalingen, maar verschillende gewichten zijn, wordt in het diagram alleen de oefening met het hoogste gewicht weergegeven.", + "@logHelpEntries": {}, + "logHelpEntriesUnits": "Let op: alleen gegevens met een gewichtseenheid (kg of lb) en herhalingen worden weergegeven; andere combinaties, zoals tijd of tot uitputting, worden hier buiten beschouwing gelaten.", + "@logHelpEntriesUnits": {}, + "description": "Beschrijving", + "@description": {}, + "name": "Naam", + "@name": { + "description": "Name for a workout or nutritional plan" + }, + "save": "Opslaan", + "@save": {}, + "verify": "Bevestig", + "@verify": {}, + "addSet": "Set toevoegen", + "@addSet": { + "description": "Label for the button that adds a set (to a workout day)" + }, + "addMeal": "Maaltijd toevoegen", + "@addMeal": {}, + "mealLogged": "Maaltijd geregistreerd in dagboek", + "@mealLogged": {}, + "ingredientLogged": "Ingrediënten geregistreerd in dagboek", + "@ingredientLogged": {}, + "logMeal": "Noteer de maaltijd in je voedingsdagboek", + "@logMeal": {}, + "addIngredient": "Voeg ingrediënt toe", + "@addIngredient": {}, + "logIngredient": "Noteer ingrediënt in je voedingsdagboek", + "@logIngredient": {}, + "searchIngredient": "Zoek ingrediënt", + "@searchIngredient": { + "description": "Label on ingredient search form" + }, + "nutritionalPlan": "Voedingsplan", + "@nutritionalPlan": {}, + "nutritionalDiary": "Voedingsdagboek", + "@nutritionalDiary": {}, + "nutritionalPlans": "Voedingsplannen", + "@nutritionalPlans": {}, + "noNutritionalPlans": "U heeft geen voedingsplannen", + "@noNutritionalPlans": { + "description": "Message shown when the user has no nutritional plans" + }, + "onlyLogging": "Alleen calorieën bijhouden", + "@onlyLogging": {}, + "onlyLoggingHelpText": "Vink dit vakje aan als u alleen uw calorieën wilt registreren en geen gedetailleerd voedingsplan met specifieke maaltijden wilt opstellen", + "@onlyLoggingHelpText": {}, + "goalMacro": "Macro doelen", + "@goalMacro": { + "description": "The goal for macronutrients" + }, + "selectMealToLog": "Selecteer een maaltijd om in je dagboek te registreren", + "@selectMealToLog": {}, + "yourCurrentNutritionPlanHasNoMealsDefinedYet": "In je huidige voedingsplan zijn geen maaltijden vastgelegd", + "@yourCurrentNutritionPlanHasNoMealsDefinedYet": { + "description": "Message shown when a nutrition plan doesn't have any meals" + }, + "toAddMealsToThePlanGoToNutritionalPlanDetails": "Om maaltijden aan het plan toe te voegen, ga naar de details van het voedingsplan", + "@toAddMealsToThePlanGoToNutritionalPlanDetails": { + "description": "Message shown to guide users to the nutritional plan details page to add meals" + }, + "goalEnergy": "Energie doel", + "@goalEnergy": {}, + "goalProtein": "Proteïne doel", + "@goalProtein": {}, + "goalCarbohydrates": "Koolhydratendoel", + "@goalCarbohydrates": {}, + "goalFat": "Vet doel", + "@goalFat": {}, + "goalFiber": "Vezel doel", + "@goalFiber": {}, + "anErrorOccurred": "Er is een fout opgetreden!", + "@anErrorOccurred": {}, + "errorInfoDescription": "Het spijt ons, maar er is iets misgegaan. Je kunt ons helpen dit op te lossen door het probleem te melden op GitHub.", + "@errorInfoDescription": {}, + "errorInfoDescription2": "Je kunt de app blijven gebruiken, maar sommige functies werken mogelijk niet.", + "@errorInfoDescription2": {}, + "errorViewDetails": "Technische details", + "@errorViewDetails": {}, + "applicationLogs": "Applicatie logboek", + "@applicationLogs": {}, + "errorCouldNotConnectToServer": "Kon niet verbinden met de server", + "@errorCouldNotConnectToServer": {}, + "errorCouldNotConnectToServerDetails": "De applicatie kon geen verbinding maken met de server. Controleer uw internetverbinding of de server-URL en probeer het opnieuw. Als het probleem aanhoudt, neem dan contact op met de serverbeheerder.", + "@errorCouldNotConnectToServerDetails": {}, + "copyToClipboard": "Kopieer naar klembord", + "@copyToClipboard": {}, + "weight": "Gewicht", + "@weight": { + "description": "The weight of a workout log or body weight entry" + }, + "min": "Min", + "@min": {}, + "max": "Max", + "@max": {}, + "chartAllTimeTitle": "{name} over gehele tijd", + "@chartAllTimeTitle": { + "description": "All-time chart of 'name' (e.g. 'weight', 'body fat' etc.)", + "type": "text", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "chart30DaysTitle": "{name} afgelopen 30 dagen", + "@chart30DaysTitle": { + "description": "last 30 days chart of 'name' (e.g. 'weight', 'body fat' etc.)", + "type": "text", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "chartDuringPlanTitle": "{chartName} tijdens voedingsplan {planName}", + "@chartDuringPlanTitle": { + "description": "chart of 'chartName' (e.g. 'weight', 'body fat' etc.) logged during plan", + "type": "text", + "placeholders": { + "chartName": { + "type": "String" + }, + "planName": { + "type": "String" + } + } + }, + "measurement": "Meting", + "@measurement": {}, + "measurements": "Metingen", + "@measurements": { + "description": "Categories for the measurements such as biceps size, body fat, etc." + }, + "measurementCategoriesHelpText": "Meetcategorie, zoals 'biceps' of 'lichaamsvet'", + "@measurementCategoriesHelpText": {}, + "measurementEntriesHelpText": "De eenheid die wordt gebruikt om de categorie te meten, zoals 'cm' of '%'", + "@measurementEntriesHelpText": {}, + "date": "Datum", + "@date": { + "description": "The date of a workout log or body weight entry" + }, + "endDate": "Einddatum", + "@endDate": { + "description": "The End date of a nutritional plan or routine" + }, + "openEnded": "Open einde", + "@openEnded": { + "description": "When a nutrition plan has no pre-defined end date" + }, + "value": "Waarde", + "@value": { + "description": "The value of a measurement entry" + }, + "start": "Start", + "@start": { + "description": "Label on button to start the gym mode (i.e., an imperative)" + }, + "time": "Tijd", + "@time": { + "description": "The time of a meal or workout" + }, + "timeStart": "Start tijd", + "@timeStart": { + "description": "The starting time of a workout" + }, + "timeEnd": "Eind tijd", + "@timeEnd": { + "description": "The end time of a workout" + }, + "timeStartAhead": "De begintijd mag niet voor de eindtijd liggen", + "@timeStartAhead": {}, + "ingredient": "Ingrediënt", + "@ingredient": {}, + "energy": "Energie", + "@energy": { + "description": "Energy in a meal, ingredient etc. e.g. in kJ" + }, + "energyShort": "E", + "@energyShort": { + "description": "The first letter or short name of the word 'Energy', used in overviews" + }, + "macronutrients": "Macronutriënten", + "@macronutrients": {}, + "planned": "Gepland", + "@planned": { + "description": "Header for the column of 'planned' nutritional values, i.e. what should be eaten" + }, + "logged": "Gelogd", + "@logged": { + "description": "Header for the column of 'logged' nutritional values, i.e. what was eaten" + }, + "today": "Vandaag", + "@today": {}, + "loggedToday": "Vandaag gelogd", + "@loggedToday": {}, + "weekAverage": "7 dagen gemiddelde", + "@weekAverage": { + "description": "Header for the column of '7 day average' nutritional values, i.e. what was logged last week" + }, + "surplus": "overschot", + "@surplus": { + "description": "Caloric surplus (either planned or unplanned)" + }, + "deficit": "tekort", + "@deficit": { + "description": "Caloric deficit (either planned or unplanned)" + }, + "difference": "Verschil", + "@difference": {}, + "percentEnergy": "Procent energie", + "@percentEnergy": {}, + "gPerBodyKg": "g per lichaams kg", + "@gPerBodyKg": { + "description": "Label used for total sums of e.g. calories or similar in grams per Kg of body weight" + }, + "total": "Totaal", + "@total": { + "description": "Label used for total sums of e.g. calories or similar" + }, + "kcal": "kcal", + "@kcal": { + "description": "Energy in a meal in kilocalories, kcal" + }, + "kcalValue": "{value} kcal", + "@kcalValue": { + "description": "A value in kcal, e.g. 500 kcal", + "type": "text", + "placeholders": { + "value": { + "type": "String" + } + } + }, + "kJ": "kJ", + "@kJ": { + "description": "Energy in a meal in kilo joules, kJ" + }, + "g": "g", + "@g": { + "description": "Abbreviation for gram" + }, + "gValue": "{value} g", + "@gValue": { + "description": "A value in grams, e.g. 5 g", + "type": "text", + "placeholders": { + "value": { + "type": "String" + } + } + }, + "percentValue": "{value} %", + "@percentValue": { + "description": "A value in percent, e.g. 10 %", + "type": "text", + "placeholders": { + "value": { + "type": "String" + } + } + }, + "protein": "Proteïne", + "@protein": {}, + "proteinShort": "P", + "@proteinShort": { + "description": "The first letter or short name of the word 'Protein', used in overviews" + }, + "carbohydrates": "Koolhydraten", + "@carbohydrates": {}, + "carbohydratesShort": "K", + "@carbohydratesShort": { + "description": "The first letter or short name of the word 'Carbohydrates', used in overviews" + }, + "sugars": "Suikers", + "@sugars": {}, + "fat": "Vet", + "@fat": {}, + "fatShort": "V", + "@fatShort": { + "description": "The first letter or short name of the word 'Fat', used in overviews" + }, + "saturatedFat": "Verzadigd vet", + "@saturatedFat": {}, + "fiber": "Vezels", + "@fiber": {}, + "sodium": "Natrium", + "@sodium": {}, + "amount": "Hoeveelheid", + "@amount": { + "description": "The amount (e.g. in grams) of an ingredient in a meal" + }, + "unit": "Eenheid", + "@unit": { + "description": "The unit used for a repetition (kg, time, etc.)" + }, + "newEntry": "Nieuwe invoer", + "@newEntry": { + "description": "Title when adding a new entry such as a weight or log entry" + }, + "noWeightEntries": "Je hebt geen gewichtsinvoer", + "@noWeightEntries": { + "description": "Message shown when the user has no logged weight entries" + }, + "noMeasurementEntries": "U heeft geen meetgegevens ingevoerd", + "@noMeasurementEntries": {}, + "moreMeasurementEntries": "Nieuwe meting toevoegen", + "@moreMeasurementEntries": { + "description": "Message shown when the user wants to add new measurement" + }, + "edit": "Wijzig", + "@edit": {}, + "loadingText": "Laden...", + "@loadingText": { + "description": "Text to show when entries are being loaded in the background: Loading..." + }, + "delete": "Verwijder", + "@delete": {}, + "confirmDelete": "Weet je zeker dat je '{toDelete}' wilt verwijderen?", + "@confirmDelete": { + "description": "Confirmation text before the user deletes an object", + "type": "text", + "placeholders": { + "toDelete": { + "type": "String" + } + } + }, + "newNutritionalPlan": "Nieuw voedingsplan", + "@newNutritionalPlan": {}, + "overview": "Overzicht", + "@overview": {}, + "toggleDetails": "Schakel details in", + "@toggleDetails": { + "description": "Switch to toggle detail / overview" + } } From 8d912090814297da2a3201c6ef68b54281d9ea9b Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 14 Dec 2025 21:45:57 +0100 Subject: [PATCH 08/54] Feat: Add German translation for Gym Mode Settings Added German translations in app_de.arb. This is my first time working with Flutter localization! --- ios/Flutter/ephemeral/flutter_lldb_helper.py | 32 ++++++++++++++++++++ ios/Flutter/ephemeral/flutter_lldbinit | 5 +++ lib/l10n/app_de.arb | 11 ++++++- 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 ios/Flutter/ephemeral/flutter_lldb_helper.py create mode 100644 ios/Flutter/ephemeral/flutter_lldbinit diff --git a/ios/Flutter/ephemeral/flutter_lldb_helper.py b/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 00000000..a88caf99 --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/ios/Flutter/ephemeral/flutter_lldbinit b/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 00000000..e3ba6fbe --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f768dcd6..d77d9ec9 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1127,5 +1127,14 @@ "endWorkout": "Training beenden", "@endWorkout": {}, "dayTypeCustom": "personalisierte", - "@dayTypeCustom": {} + "@dayTypeCustom": {}, + "gymModeShowExercises": "Übersichtsseiten anzeigen", + "gymModeShowTimer": "Timer zwischen Sätzen anzeigen", + "gymModeTimerType": "Timer-Typ", + "gymModeTimerTypeHelText": "Wenn eine Satzpause eingegeben ist, wird immer ein Countdown genutzt.", + "countdown": "Countdown", + "stopwatch": "Stoppuhr", + "gymModeDefaultCountdownTime": "Standard-Countdown (Sekunden)", + "gymModeNotifyOnCountdownFinish": "Benachrichtigung bei Ablauf", + "duration": "Dauer" } From 4b04b2b89a21bc80f520acefdbfb1170b1b1ab14 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sun, 14 Dec 2025 22:08:22 +0100 Subject: [PATCH 09/54] Correctly handle state change in LogFormWidget This was causing the logs from a previous exercise being displayed and saved --- lib/widgets/routines/gym_mode/log_page.dart | 69 ++++++++++++++++----- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index d4fa2575..007dbcea 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, 2021 wger Team + * Copyright (C) 2020, 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -15,6 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; @@ -50,7 +51,7 @@ class LogPage extends ConsumerStatefulWidget { } class _LogPageState extends ConsumerState { - final GlobalKey<_LogFormWidgetState> _logFormKey = GlobalKey<_LogFormWidgetState>(); + late void Function(Log) _copyFromPastLog = (_) {}; late FocusNode focusNode; @@ -70,6 +71,7 @@ class _LogPageState extends ConsumerState { Widget build(BuildContext context) { final theme = Theme.of(context); final state = ref.watch(gymStateProvider); + final languageCode = Localizations.localeOf(context).languageCode; final page = state.getPageByIndex(); if (page == null) { @@ -101,7 +103,7 @@ class _LogPageState extends ConsumerState { return Column( children: [ NavigationHeader( - log.exercise.getTranslation(Localizations.localeOf(context).languageCode).name, + log.exercise.getTranslation(languageCode).name, widget._controller, ), @@ -153,7 +155,8 @@ class _LogPageState extends ConsumerState { log: log, pastLogs: state.routine.filterLogsByExercise(log.exercise.id!), onCopy: (pastLog) { - _logFormKey.currentState?.copyFromPastLog(pastLog); + // Call the function registered by the child + _copyFromPastLog(pastLog); }, setStateCallback: (fn) { setState(fn); @@ -170,11 +173,11 @@ class _LogPageState extends ConsumerState { child: Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: LogFormWidget( - key: _logFormKey, controller: widget._controller, configData: setConfigData, log: log, focusNode: focusNode, + registerCopy: (fn) => _copyFromPastLog = fn, ), ), ), @@ -502,6 +505,8 @@ class LogFormWidget extends ConsumerStatefulWidget { final SetConfigData configData; final Log log; final FocusNode focusNode; + // Callback used by the child to register its copy function with the parent. + final void Function(void Function(Log))? registerCopy; LogFormWidget({ super.key, @@ -509,6 +514,7 @@ class LogFormWidget extends ConsumerStatefulWidget { required this.configData, required this.log, required this.focusNode, + this.registerCopy, }); @override @@ -532,20 +538,53 @@ class _LogFormWidgetState extends ConsumerState { _repetitionsController = TextEditingController(); _weightController = TextEditingController(); + // Register the copy function with the parent + widget.registerCopy?.call(copyFromPastLog); + WidgetsBinding.instance.addPostFrameCallback((_) { - final locale = Localizations.localeOf(context).toString(); - final numberFormat = NumberFormat.decimalPattern(locale); - - if (widget.configData.repetitions != null) { - _repetitionsController.text = numberFormat.format(widget.configData.repetitions); - } - - if (widget.configData.weight != null) { - _weightController.text = numberFormat.format(widget.configData.weight); - } + _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(); + } + + // If the parent replaced the registerCopy callback, register again + if (oldWidget.registerCopy != widget.registerCopy) { + widget.registerCopy?.call(copyFromPastLog); + } + } + + 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.fine('Error syncing controllers: $e'); + _repetitionsController.text = ''; + _weightController.text = ''; + } + } + @override void dispose() { _repetitionsController.dispose(); From 63e67a686f82e4e74dadea88837e621aed999ac3 Mon Sep 17 00:00:00 2001 From: Diego Menezes Date: Thu, 4 Dec 2025 21:05:53 +0100 Subject: [PATCH 10/54] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (369 of 369 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/pt_BR/ --- lib/l10n/app_pt_BR.arb | 204 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 186 insertions(+), 18 deletions(-) diff --git a/lib/l10n/app_pt_BR.arb b/lib/l10n/app_pt_BR.arb index 85ff9ccf..a21afbd4 100644 --- a/lib/l10n/app_pt_BR.arb +++ b/lib/l10n/app_pt_BR.arb @@ -115,7 +115,7 @@ "@close": { "description": "Translation for close" }, - "successfullyDeleted": "Excluído", + "successfullyDeleted": "Excluído com êxito", "@successfullyDeleted": { "description": "Message when an item was successfully deleted" }, @@ -125,13 +125,13 @@ "@goToToday": { "description": "Label on button to jump back to 'today' in the calendar widget" }, - "set": "Definir", + "set": "Série", "@set": { "description": "A set in a workout plan" }, "noMeasurementEntries": "Você não tem entradas de medição", "@noMeasurementEntries": {}, - "newSet": "Novo conjunto", + "newSet": "Nova séries", "@newSet": { "description": "Header when adding a new set to a workout day" }, @@ -189,7 +189,7 @@ "@pause": { "description": "Noun, not an imperative! Label used for the pause when using the gym mode" }, - "success": "Sucesso", + "success": "Êxito", "@success": { "description": "Message when an action completed successfully, usually used as a heading" }, @@ -213,7 +213,7 @@ "@newEntry": { "description": "Title when adding a new entry such as a weight or log entry" }, - "addSet": "Adicionar set", + "addSet": "Adicionar séries", "@addSet": { "description": "Label for the button that adds a set (to a workout day)" }, @@ -271,7 +271,7 @@ "@loadingText": { "description": "Text to show when entries are being loaded in the background: Loading..." }, - "selectExercises": "Se quiser fazer um superset você pode procurar vários exercícios, eles estarão agrupados", + "selectExercises": "Se quiser fazer um superséries você pode procurar vários exercícios, eles estarão agrupados", "@selectExercises": {}, "nutritionalDiary": "Diário nutricional", "@nutritionalDiary": {}, @@ -953,7 +953,7 @@ "@noRoutines": {}, "restTime": "Tempo de descanso", "@restTime": {}, - "sets": "Conjuntos", + "sets": "Séries", "@sets": { "description": "The number of sets to be done for one exercise" }, @@ -967,7 +967,7 @@ } } }, - "supersetNr": "Superset {nr}", + "supersetNr": "Supersérie {nr}", "@supersetNr": { "description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Superset Nr. xy'.", "type": "text", @@ -981,7 +981,7 @@ "@restDay": {}, "isRestDay": "É dia de descanso", "@isRestDay": {}, - "isRestDayHelp": "Por favor, note que todos os conjuntos e exercícios serão removidos quando marcar um dia como um dia de descanso.", + "isRestDayHelp": "Por favor, note que todos as séries e exercícios serão removidos quando marcar um dia como um dia de descanso.", "@isRestDayHelp": {}, "needsLogsToAdvance": "Precisa de logs para avançar", "@needsLogsToAdvance": {}, @@ -1005,7 +1005,7 @@ "@toAddMealsToThePlanGoToNutritionalPlanDetails": { "description": "Message shown to guide users to the nutritional plan details page to add meals" }, - "errorInfoDescription": "Algo de errado aconteceu. Você pode nos ajudar a concertar esse problema reportando o problema no Github", + "errorInfoDescription": "Algo de errado aconteceu. Você pode nos ajudar a concertar esse problema reportando o problema no Github.", "@errorInfoDescription": {}, "errorInfoDescription2": "Você pode continuar usando o applicativo, mas algumas funcionalidades não estarão disponíveis.", "@errorInfoDescription2": {}, @@ -1021,7 +1021,7 @@ "@min": {}, "max": "Max", "@max": {}, - "aboutWhySupportTitle": "Open Source & free to use ❤️", + "aboutWhySupportTitle": "Código aberto e de uso gratuito ❤️", "@aboutWhySupportTitle": {}, "aboutContributeTitle": "Contribua", "@aboutContributeTitle": {}, @@ -1043,13 +1043,13 @@ "@fitInWeek": {}, "fitInWeekHelp": "Se ligado, os dias vão se repetir semanalmente, caso contrário os dias seguirão sequencialmente se considerar o começo de uma nova semana.", "@fitInWeekHelp": {}, - "addSuperset": "Adicionar superset", + "addSuperset": "Adicionar superséries", "@addSuperset": {}, "setHasProgression": "Treino tem prograssão", "@setHasProgression": {}, - "setHasProgressionWarning": "Observe que, no momento, não é possível editar todas as configurações de um conjunto no aplicativo móvel nem configurar a progressão automática. Por enquanto, use o aplicativo web.", + "setHasProgressionWarning": "Observe que, no momento, não é possível editar todas as configurações de um séries no aplicativo móvel nem configurar a progressão automática. Por enquanto, use o aplicativo web.", "@setHasProgressionWarning": {}, - "setHasNoExercises": "Este treino ainda não tem exercícios!", + "setHasNoExercises": "Este séries ainda não tem exercícios!", "@setHasNoExercises": {}, "simpleMode": "Modo simples", "@simpleMode": {}, @@ -1057,7 +1057,7 @@ "@simpleModeHelp": {}, "progressionRules": "Este exercício tem regras de progressão e não pode ser editado no aplicativo móvel. Use o aplicativo web para editá-lo.", "@progressionRules": {}, - "resistance_band": "Resistance band", + "resistance_band": "Banda de resistência", "@resistance_band": { "description": "Generated entry for translation for server strings" }, @@ -1077,8 +1077,176 @@ "@startDate": {}, "dayTypeCustom": "Personalizado", "@dayTypeCustom": {}, - "dayTypeHiit": "Treino de alta intensidade", + "dayTypeHiit": "Treinamento intervalado de alta intensidade", "@dayTypeHiit": {}, - "dayTypeTabata": "Tabata", - "@dayTypeTabata": {} + "dayTypeTabata": "Método Tabata", + "@dayTypeTabata": {}, + "impressionGood": "Boa", + "@impressionGood": {}, + "impressionNeutral": "Neutra", + "@impressionNeutral": {}, + "impressionBad": "Ruim", + "@impressionBad": {}, + "gymModeShowExercises": "Mostrar páginas de visão geral dos exercícios", + "@gymModeShowExercises": {}, + "gymModeShowTimer": "Mostrar cronômetro entre séries", + "@gymModeShowTimer": {}, + "gymModeTimerType": "Tipo de temporizador", + "@gymModeTimerType": {}, + "gymModeTimerTypeHelText": "Se uma série tiver tempo de pausa, sempre será usada uma contagem regressiva.", + "@gymModeTimerTypeHelText": {}, + "countdown": "Contagem regressiva", + "@countdown": {}, + "stopwatch": "cronômetro", + "@stopwatch": {}, + "gymModeDefaultCountdownTime": "Tempo de contagem regressiva padrão, em segundos", + "@gymModeDefaultCountdownTime": {}, + "gymModeNotifyOnCountdownFinish": "Notificar no final da contagem regressiva", + "@gymModeNotifyOnCountdownFinish": {}, + "duration": "Duração", + "@duration": {}, + "durationHoursMinutes": "{hours}h {minutes}m", + "@durationHoursMinutes": { + "description": "A duration, in hours and minutes", + "type": "text", + "placeholders": { + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + } + } + }, + "volume": "Volume", + "@volume": { + "description": "The volume of a workout or set, i.e. weight x reps" + }, + "workoutCompleted": "Treino concluído", + "@workoutCompleted": {}, + "dayTypeEnom": "Cada minuto a minuto", + "@dayTypeEnom": {}, + "dayTypeAmrap": "Tantas rodadas quanto possível", + "@dayTypeAmrap": {}, + "dayTypeEdt": "Treinamento de densidade crescente", + "@dayTypeEdt": {}, + "dayTypeRft": "Rodadas para ganhar tempo", + "@dayTypeRft": {}, + "dayTypeAfap": "O mais rápido possível", + "@dayTypeAfap": {}, + "slotEntryTypeNormal": "Normal", + "@slotEntryTypeNormal": {}, + "slotEntryTypePartial": "Parcial", + "@slotEntryTypePartial": {}, + "slotEntryTypeForced": "Forçado", + "@slotEntryTypeForced": {}, + "slotEntryTypeTut": "Tempo Sob Tensão", + "@slotEntryTypeTut": {}, + "slotEntryTypeIso": "Fixação isométrica", + "@slotEntryTypeIso": {}, + "slotEntryTypeJump": "Pular", + "@slotEntryTypeJump": {}, + "applicationLogs": "Registros de aplicativos", + "@applicationLogs": {}, + "openEnded": "Aberto", + "@openEnded": { + "description": "When a nutrition plan has no pre-defined end date" + }, + "overview": "visão global", + "@overview": {}, + "formMinMaxValues": "Insira um valor entre {min} e {max}", + "@formMinMaxValues": { + "description": "Error message when the user needs to enter a value between min and max", + "type": "text", + "placeholders": { + "min": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + "identicalExercisePleaseDiscard": "Se você notar um exercício idêntico ao que você está adicionando, descarte o rascunho e edite o exercício.", + "@identicalExercisePleaseDiscard": {}, + "checkInformationBeforeSubmitting": "Verifique se as informações inseridas estão corretas antes de enviar o exercício", + "@checkInformationBeforeSubmitting": {}, + "imageDetailsTitle": "Detalhes da imagem", + "@imageDetailsTitle": { + "description": "Title for image details form" + }, + "imageDetailsLicenseTitle": "Titulo", + "@imageDetailsLicenseTitle": { + "description": "Label for image title field" + }, + "imageDetailsLicenseTitleHint": "Insira o título da imagem", + "@imageDetailsLicenseTitleHint": { + "description": "Hint text for image title field" + }, + "imageDetailsSourceLink": "Link para o site de origem", + "@imageDetailsSourceLink": { + "description": "Label for source link field" + }, + "author": "Autor(s)", + "@author": {}, + "authorHint": "Digite o nome do autor", + "@authorHint": { + "description": "Hint text for author field" + }, + "imageDetailsAuthorLink": "Link para o site ou perfil do autor", + "@imageDetailsAuthorLink": { + "description": "Label for author link field" + }, + "imageDetailsDerivativeSource": "Link para a fonte original, se este for um trabalho derivado", + "@imageDetailsDerivativeSource": { + "description": "Label for derivative source field" + }, + "imageDetailsDerivativeHelp": "Um trabalho derivado é baseado em um trabalho anterior, mas contém conteúdo novo e criativo suficiente para ter direito aos seus próprios direitos autorais.", + "@imageDetailsDerivativeHelp": { + "description": "Helper text explaining derivative works" + }, + "imageDetailsImageType": "Tipo de imagem", + "@imageDetailsImageType": { + "description": "Label for image type selector" + }, + "imageDetailsLicenseNotice": "Ao enviar esta imagem, você concorda em liberá-la sob CC-BY-SA-4. A imagem deve ser de sua autoria ou o autor deve tê-la divulgado sob uma licença compatível com ela.", + "@imageDetailsLicenseNotice": {}, + "imageDetailsLicenseNoticeLinkToLicense": "Consulte o texto da licença.", + "@imageDetailsLicenseNoticeLinkToLicense": {}, + "imageFormatNotSupported": "{imageFormat} não compatível", + "@imageFormatNotSupported": { + "description": "Label shown on the error container when image format is not supported", + "type": "text", + "placeholders": { + "imageFormat": { + "type": "String" + } + } + }, + "imageFormatNotSupportedDetail": "Imagens {imageFormat} ainda não são suportadas.", + "@imageFormatNotSupportedDetail": { + "description": "Label shown on the image preview container when image format is not supported", + "type": "text", + "placeholders": { + "imageFormat": { + "type": "String" + } + } + }, + "add": "adicionar", + "@add": { + "description": "Add button text" + }, + "superset": "Supersérie", + "@superset": {}, + "enterTextInLanguage": "Por favor, insira o texto no idioma correto!", + "@enterTextInLanguage": {}, + "endWorkout": "Terminar treino", + "@endWorkout": { + "description": "Use the imperative, label on button to finish the current workout in gym mode" + }, + "slotEntryTypeMyo": "Myo", + "@slotEntryTypeMyo": {}, + "slotEntryTypeDropset": "Drop set", + "@slotEntryTypeDropset": {} } From bf38d01fe37732d957ff8fa47761db5e472ad349 Mon Sep 17 00:00:00 2001 From: Diego Menezes Date: Sat, 6 Dec 2025 02:20:18 +0100 Subject: [PATCH 11/54] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (369 of 369 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/pt_BR/ --- lib/l10n/app_pt_BR.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_pt_BR.arb b/lib/l10n/app_pt_BR.arb index a21afbd4..2e3d2e26 100644 --- a/lib/l10n/app_pt_BR.arb +++ b/lib/l10n/app_pt_BR.arb @@ -115,7 +115,7 @@ "@close": { "description": "Translation for close" }, - "successfullyDeleted": "Excluído com êxito", + "successfullyDeleted": "Removido com êxito", "@successfullyDeleted": { "description": "Message when an item was successfully deleted" }, @@ -189,7 +189,7 @@ "@pause": { "description": "Noun, not an imperative! Label used for the pause when using the gym mode" }, - "success": "Êxito", + "success": "Bem-sucedido", "@success": { "description": "Message when an action completed successfully, usually used as a heading" }, From 68ea2042a6532f24a48719151dd63899cab86120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=93=D0=BE=D1=80?= =?UTF-8?q?=D0=BF=D0=B8=D0=BD=D1=96=D1=87?= Date: Mon, 8 Dec 2025 04:50:58 +0100 Subject: [PATCH 12/54] Translated using Weblate (Ukrainian) Currently translated at 100.0% (369 of 369 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/uk/ --- lib/l10n/app_uk.arb | 60 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 1d88c768..243d31f6 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1162,5 +1162,63 @@ "slotEntryTypeJump": "Стрибок", "@slotEntryTypeJump": {}, "endWorkout": "Закінчити тренування", - "@endWorkout": {} + "@endWorkout": {}, + "impressionGood": "Добре", + "@impressionGood": {}, + "impressionNeutral": "Нейтральний", + "@impressionNeutral": {}, + "impressionBad": "Погано", + "@impressionBad": {}, + "gymModeShowExercises": "Показати сторінки огляду вправ", + "@gymModeShowExercises": {}, + "gymModeShowTimer": "Показувати таймер між сетами", + "@gymModeShowTimer": {}, + "gymModeTimerType": "Тип таймера", + "@gymModeTimerType": {}, + "gymModeTimerTypeHelText": "Якщо сет має час паузи, завжди використовується зворотний відлік.", + "@gymModeTimerTypeHelText": {}, + "countdown": "Зворотний відлік", + "@countdown": {}, + "stopwatch": "Секундомір", + "@stopwatch": {}, + "gymModeDefaultCountdownTime": "Час зворотного відліку за замовчуванням, у секундах", + "@gymModeDefaultCountdownTime": {}, + "gymModeNotifyOnCountdownFinish": "Повідомити про закінчення зворотного відліку", + "@gymModeNotifyOnCountdownFinish": {}, + "duration": "Тривалість", + "@duration": {}, + "durationHoursMinutes": "{hours}г {minutes}хв", + "@durationHoursMinutes": { + "description": "A duration, in hours and minutes", + "type": "text", + "placeholders": { + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + } + } + }, + "volume": "Обсяг", + "@volume": { + "description": "The volume of a workout or set, i.e. weight x reps" + }, + "workoutCompleted": "Тренування завершено", + "@workoutCompleted": {}, + "formMinMaxValues": "Будь ласка, введіть значення від {min} до {max}", + "@formMinMaxValues": { + "description": "Error message when the user needs to enter a value between min and max", + "type": "text", + "placeholders": { + "min": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + "superset": "Суперсет", + "@superset": {} } From 0787ec1c30e86cd4ac053d26dfe1c9536c0bee60 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sun, 14 Dec 2025 22:16:43 +0100 Subject: [PATCH 13/54] Translated using Weblate (German) Currently translated at 100.0% (369 of 369 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/de/ # Conflicts: # lib/l10n/app_de.arb --- lib/l10n/app_de.arb | 71 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index d77d9ec9..5c98ddd0 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -705,7 +705,7 @@ "@whatVariationsExist": {}, "previous": "Vorherige", "@previous": {}, - "next": "Nächste", + "next": "Weiter", "@next": {}, "swiss_ball": "Gymnastikball", "@swiss_ball": {}, @@ -1128,13 +1128,64 @@ "@endWorkout": {}, "dayTypeCustom": "personalisierte", "@dayTypeCustom": {}, - "gymModeShowExercises": "Übersichtsseiten anzeigen", - "gymModeShowTimer": "Timer zwischen Sätzen anzeigen", - "gymModeTimerType": "Timer-Typ", - "gymModeTimerTypeHelText": "Wenn eine Satzpause eingegeben ist, wird immer ein Countdown genutzt.", - "countdown": "Countdown", - "stopwatch": "Stoppuhr", - "gymModeDefaultCountdownTime": "Standard-Countdown (Sekunden)", - "gymModeNotifyOnCountdownFinish": "Benachrichtigung bei Ablauf", - "duration": "Dauer" + "dayTypeTabata": "Tabata", + "@dayTypeTabata": {}, + "impressionGood": "Gut", + "@impressionGood": {}, + "impressionNeutral": "Neutral", + "@impressionNeutral": {}, + "impressionBad": "Schlecht", + "@impressionBad": {}, + "gymModeShowExercises": "Übersichtsseiten der Übungen anzeigen", + "@gymModeShowExercises": {}, + "gymModeShowTimer": "Timer zwischen Sätzen anzeigen", + "@gymModeShowTimer": {}, + "gymModeTimerType": "Timer-Typ", + "@gymModeTimerType": {}, + "gymModeTimerTypeHelText": "Wenn ein Satz eine Pausenzeit hat, wird immer ein Countdown genutzt.", + "@gymModeTimerTypeHelText": {}, + "countdown": "Countdown", + "@countdown": {}, + "stopwatch": "Stoppuhr", + "@stopwatch": {}, + "gymModeDefaultCountdownTime": "Standard-Countdown-Zeit in Sekunden", + "@gymModeDefaultCountdownTime": {}, + "gymModeNotifyOnCountdownFinish": "Benachrichtigung bei Ende des Countdowns", + "@gymModeNotifyOnCountdownFinish": {}, + "duration": "Dauer", + "@duration": {}, + "durationHoursMinutes": "{hours}h {minutes}m", + "@durationHoursMinutes": { + "description": "A duration, in hours and minutes", + "type": "text", + "placeholders": { + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + } + } + }, + "volume": "Volumen", + "@volume": { + "description": "The volume of a workout or set, i.e. weight x reps" + }, + "workoutCompleted": "Training abgeschlossen", + "@workoutCompleted": {}, + "formMinMaxValues": "Bitte geben Sie einen Wert zwischen {min} und {max}", + "@formMinMaxValues": { + "description": "Error message when the user needs to enter a value between min and max", + "type": "text", + "placeholders": { + "min": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + "superset": "Superset", + "@superset": {} } From 0819a2dd2e99ebe94a058698376c15997784a64e Mon Sep 17 00:00:00 2001 From: Floris C Date: Fri, 12 Dec 2025 16:55:25 +0100 Subject: [PATCH 14/54] Translated using Weblate (Dutch) Currently translated at 58.5% (216 of 369 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/nl/ --- lib/l10n/app_nl.arb | 502 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 501 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 4e3b3012..6da9385a 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -194,5 +194,505 @@ "slotEntryTypeNormal": "Normaal", "@slotEntryTypeNormal": {}, "slotEntryTypeDropset": "Dropset", - "@slotEntryTypeDropset": {} + "@slotEntryTypeDropset": {}, + "slotEntryTypePartial": "Deels", + "@slotEntryTypePartial": {}, + "slotEntryTypeForced": "Verplicht", + "@slotEntryTypeForced": {}, + "slotEntryTypeTut": "Tijd onder spanning", + "@slotEntryTypeTut": {}, + "slotEntryTypeIso": "Isometrische houding", + "@slotEntryTypeIso": {}, + "slotEntryTypeJump": "Springen", + "@slotEntryTypeJump": {}, + "routines": "Routines", + "@routines": {}, + "newRoutine": "Nieuwe routine", + "@newRoutine": {}, + "noRoutines": "U heeft geen routines", + "@noRoutines": {}, + "restTime": "Rust tijd", + "@restTime": {}, + "sets": "Sets", + "@sets": { + "description": "The number of sets to be done for one exercise" + }, + "rir": "RiR", + "@rir": { + "description": "Shorthand for Repetitions In Reserve" + }, + "rirNotUsed": "Ongebruikt RiR", + "@rirNotUsed": { + "description": "Label used in RiR slider when the RiR value is not used/saved for the current setting or log" + }, + "useMetric": "Gebruik metrische eenheden voor lichaamsgewicht", + "@useMetric": {}, + "weightUnit": "Gewichtseenheid", + "@weightUnit": {}, + "repetitionUnit": "Herhalingseenheid", + "@repetitionUnit": {}, + "set": "Set", + "@set": { + "description": "A set in a workout plan" + }, + "dayDescriptionHelp": "Een beschrijving van wat er deze dag is gedaan (b.v. 'pull dag') of welk lichaamsdeel getraind is (b.v. 'borst en schouders')", + "@dayDescriptionHelp": {}, + "exerciseNr": "Oefening {nr}", + "@exerciseNr": { + "description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Set Nr. xy'.", + "type": "text", + "placeholders": { + "nr": { + "type": "String" + } + } + }, + "supersetNr": "Superset {nr}", + "@supersetNr": { + "description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Superset Nr. xy'.", + "type": "text", + "placeholders": { + "nr": { + "type": "String" + } + } + }, + "sameRepetitions": "Als je dezelfde herhalingen en gewichten gebruikt voor alle sets kan je één rij invullen. Voor 4 sets kan je bijvoorbeeld 10 invullen voor de herhalingen, dit wordt dan automatisch \"4 x 10\".", + "@sameRepetitions": {}, + "impressionGood": "Goed", + "@impressionGood": {}, + "impressionNeutral": "Neutraal", + "@impressionNeutral": {}, + "impressionBad": "Slecht", + "@impressionBad": {}, + "impression": "Impressie", + "@impression": { + "description": "General impression (e.g. for a workout session) such as good, bad, etc." + }, + "notes": "Notities", + "@notes": { + "description": "Personal notes, e.g. for a workout session" + }, + "workoutSession": "Workout sessie", + "@workoutSession": { + "description": "A (logged) workout session" + }, + "restDay": "Rust dag", + "@restDay": {}, + "isRestDay": "Is rust dag", + "@isRestDay": {}, + "isRestDayHelp": "Houd er rekening mee dat alle sets en oefeningen verwijderd worden als u deze dag markeert als rust dag.", + "@isRestDayHelp": {}, + "needsLogsToAdvance": "Vereist logs om door te gaan", + "@needsLogsToAdvance": {}, + "needsLogsToAdvanceHelp": "Selecteer als je wilt dat de routine alleen doorgaat naar de volgende dag als je voor de dag een workout hebt vastgelegd", + "@needsLogsToAdvanceHelp": {}, + "routineDays": "Dagen in routine", + "@routineDays": {}, + "resultingRoutine": "Resulterende routine", + "@resultingRoutine": {}, + "newDay": "Nieuwe dag", + "@newDay": {}, + "newSet": "Nieuwe set", + "@newSet": { + "description": "Header when adding a new set to a workout day" + }, + "selectExercises": "Als je een superset wilt doen kan je naar verschillende oefeningen zoeken, en kan je ze samen groeperen", + "@selectExercises": {}, + "gymMode": "Gym modus", + "@gymMode": { + "description": "Label when starting the gym mode" + }, + "gymModeShowExercises": "Toon oefeningsoverzicht paginas", + "@gymModeShowExercises": {}, + "gymModeShowTimer": "Toon timer tussen sets", + "@gymModeShowTimer": {}, + "gymModeTimerType": "Timer type", + "@gymModeTimerType": {}, + "gymModeTimerTypeHelText": "Als een set pauze tijd heeft, wordt altijd een afteller gebruikt.", + "@gymModeTimerTypeHelText": {}, + "countdown": "Afteller", + "@countdown": {}, + "stopwatch": "Stopwatch", + "@stopwatch": {}, + "gymModeDefaultCountdownTime": "Standaard afteltijd, in secondes", + "@gymModeDefaultCountdownTime": {}, + "gymModeNotifyOnCountdownFinish": "Notificeer op einde aftelling", + "@gymModeNotifyOnCountdownFinish": {}, + "duration": "Duur", + "@duration": {}, + "durationHoursMinutes": "{hours}u {minutes}m", + "@durationHoursMinutes": { + "description": "A duration, in hours and minutes", + "type": "text", + "placeholders": { + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + } + } + }, + "volume": "Volume", + "@volume": { + "description": "The volume of a workout or set, i.e. weight x reps" + }, + "workoutCompleted": "Workout voltooid", + "@workoutCompleted": {}, + "plateCalculator": "Platen", + "@plateCalculator": { + "description": "Label used for the plate calculator in the gym mode" + }, + "plateCalculatorNotDivisible": "Niet mogelijk om gewicht te bereiken met beschikbare platen", + "@plateCalculatorNotDivisible": { + "description": "Error message when the current weight is not reachable with plates (e.g. 33.1 kg)" + }, + "pause": "Pauzeer", + "@pause": { + "description": "Noun, not an imperative! Label used for the pause when using the gym mode" + }, + "jumpTo": "Ga naar", + "@jumpTo": { + "description": "Imperative. Label used in popup allowing the user to jump to a specific exercise while in the gym mode" + }, + "todaysWorkout": "Uw workout vandaag", + "@todaysWorkout": {}, + "logHelpEntries": "Als er op een enkele dag meerdere oefeningen met hetzelfde aantal herhalingen, maar verschillende gewichten zijn, wordt in het diagram alleen de oefening met het hoogste gewicht weergegeven.", + "@logHelpEntries": {}, + "logHelpEntriesUnits": "Let op: alleen gegevens met een gewichtseenheid (kg of lb) en herhalingen worden weergegeven; andere combinaties, zoals tijd of tot uitputting, worden hier buiten beschouwing gelaten.", + "@logHelpEntriesUnits": {}, + "description": "Beschrijving", + "@description": {}, + "name": "Naam", + "@name": { + "description": "Name for a workout or nutritional plan" + }, + "save": "Opslaan", + "@save": {}, + "verify": "Bevestig", + "@verify": {}, + "addSet": "Set toevoegen", + "@addSet": { + "description": "Label for the button that adds a set (to a workout day)" + }, + "addMeal": "Maaltijd toevoegen", + "@addMeal": {}, + "mealLogged": "Maaltijd geregistreerd in dagboek", + "@mealLogged": {}, + "ingredientLogged": "Ingrediënten geregistreerd in dagboek", + "@ingredientLogged": {}, + "logMeal": "Noteer de maaltijd in je voedingsdagboek", + "@logMeal": {}, + "addIngredient": "Voeg ingrediënt toe", + "@addIngredient": {}, + "logIngredient": "Noteer ingrediënt in je voedingsdagboek", + "@logIngredient": {}, + "searchIngredient": "Zoek ingrediënt", + "@searchIngredient": { + "description": "Label on ingredient search form" + }, + "nutritionalPlan": "Voedingsplan", + "@nutritionalPlan": {}, + "nutritionalDiary": "Voedingsdagboek", + "@nutritionalDiary": {}, + "nutritionalPlans": "Voedingsplannen", + "@nutritionalPlans": {}, + "noNutritionalPlans": "U heeft geen voedingsplannen", + "@noNutritionalPlans": { + "description": "Message shown when the user has no nutritional plans" + }, + "onlyLogging": "Alleen calorieën bijhouden", + "@onlyLogging": {}, + "onlyLoggingHelpText": "Vink dit vakje aan als u alleen uw calorieën wilt registreren en geen gedetailleerd voedingsplan met specifieke maaltijden wilt opstellen", + "@onlyLoggingHelpText": {}, + "goalMacro": "Macro doelen", + "@goalMacro": { + "description": "The goal for macronutrients" + }, + "selectMealToLog": "Selecteer een maaltijd om in je dagboek te registreren", + "@selectMealToLog": {}, + "yourCurrentNutritionPlanHasNoMealsDefinedYet": "In je huidige voedingsplan zijn geen maaltijden vastgelegd", + "@yourCurrentNutritionPlanHasNoMealsDefinedYet": { + "description": "Message shown when a nutrition plan doesn't have any meals" + }, + "toAddMealsToThePlanGoToNutritionalPlanDetails": "Om maaltijden aan het plan toe te voegen, ga naar de details van het voedingsplan", + "@toAddMealsToThePlanGoToNutritionalPlanDetails": { + "description": "Message shown to guide users to the nutritional plan details page to add meals" + }, + "goalEnergy": "Energie doel", + "@goalEnergy": {}, + "goalProtein": "Proteïne doel", + "@goalProtein": {}, + "goalCarbohydrates": "Koolhydratendoel", + "@goalCarbohydrates": {}, + "goalFat": "Vet doel", + "@goalFat": {}, + "goalFiber": "Vezel doel", + "@goalFiber": {}, + "anErrorOccurred": "Er is een fout opgetreden!", + "@anErrorOccurred": {}, + "errorInfoDescription": "Het spijt ons, maar er is iets misgegaan. Je kunt ons helpen dit op te lossen door het probleem te melden op GitHub.", + "@errorInfoDescription": {}, + "errorInfoDescription2": "Je kunt de app blijven gebruiken, maar sommige functies werken mogelijk niet.", + "@errorInfoDescription2": {}, + "errorViewDetails": "Technische details", + "@errorViewDetails": {}, + "applicationLogs": "Applicatie logboek", + "@applicationLogs": {}, + "errorCouldNotConnectToServer": "Kon niet verbinden met de server", + "@errorCouldNotConnectToServer": {}, + "errorCouldNotConnectToServerDetails": "De applicatie kon geen verbinding maken met de server. Controleer uw internetverbinding of de server-URL en probeer het opnieuw. Als het probleem aanhoudt, neem dan contact op met de serverbeheerder.", + "@errorCouldNotConnectToServerDetails": {}, + "copyToClipboard": "Kopieer naar klembord", + "@copyToClipboard": {}, + "weight": "Gewicht", + "@weight": { + "description": "The weight of a workout log or body weight entry" + }, + "min": "Min", + "@min": {}, + "max": "Max", + "@max": {}, + "chartAllTimeTitle": "{name} over gehele tijd", + "@chartAllTimeTitle": { + "description": "All-time chart of 'name' (e.g. 'weight', 'body fat' etc.)", + "type": "text", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "chart30DaysTitle": "{name} afgelopen 30 dagen", + "@chart30DaysTitle": { + "description": "last 30 days chart of 'name' (e.g. 'weight', 'body fat' etc.)", + "type": "text", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "chartDuringPlanTitle": "{chartName} tijdens voedingsplan {planName}", + "@chartDuringPlanTitle": { + "description": "chart of 'chartName' (e.g. 'weight', 'body fat' etc.) logged during plan", + "type": "text", + "placeholders": { + "chartName": { + "type": "String" + }, + "planName": { + "type": "String" + } + } + }, + "measurement": "Meting", + "@measurement": {}, + "measurements": "Metingen", + "@measurements": { + "description": "Categories for the measurements such as biceps size, body fat, etc." + }, + "measurementCategoriesHelpText": "Meetcategorie, zoals 'biceps' of 'lichaamsvet'", + "@measurementCategoriesHelpText": {}, + "measurementEntriesHelpText": "De eenheid die wordt gebruikt om de categorie te meten, zoals 'cm' of '%'", + "@measurementEntriesHelpText": {}, + "date": "Datum", + "@date": { + "description": "The date of a workout log or body weight entry" + }, + "endDate": "Einddatum", + "@endDate": { + "description": "The End date of a nutritional plan or routine" + }, + "openEnded": "Open einde", + "@openEnded": { + "description": "When a nutrition plan has no pre-defined end date" + }, + "value": "Waarde", + "@value": { + "description": "The value of a measurement entry" + }, + "start": "Start", + "@start": { + "description": "Label on button to start the gym mode (i.e., an imperative)" + }, + "time": "Tijd", + "@time": { + "description": "The time of a meal or workout" + }, + "timeStart": "Start tijd", + "@timeStart": { + "description": "The starting time of a workout" + }, + "timeEnd": "Eind tijd", + "@timeEnd": { + "description": "The end time of a workout" + }, + "timeStartAhead": "De begintijd mag niet voor de eindtijd liggen", + "@timeStartAhead": {}, + "ingredient": "Ingrediënt", + "@ingredient": {}, + "energy": "Energie", + "@energy": { + "description": "Energy in a meal, ingredient etc. e.g. in kJ" + }, + "energyShort": "E", + "@energyShort": { + "description": "The first letter or short name of the word 'Energy', used in overviews" + }, + "macronutrients": "Macronutriënten", + "@macronutrients": {}, + "planned": "Gepland", + "@planned": { + "description": "Header for the column of 'planned' nutritional values, i.e. what should be eaten" + }, + "logged": "Gelogd", + "@logged": { + "description": "Header for the column of 'logged' nutritional values, i.e. what was eaten" + }, + "today": "Vandaag", + "@today": {}, + "loggedToday": "Vandaag gelogd", + "@loggedToday": {}, + "weekAverage": "7 dagen gemiddelde", + "@weekAverage": { + "description": "Header for the column of '7 day average' nutritional values, i.e. what was logged last week" + }, + "surplus": "overschot", + "@surplus": { + "description": "Caloric surplus (either planned or unplanned)" + }, + "deficit": "tekort", + "@deficit": { + "description": "Caloric deficit (either planned or unplanned)" + }, + "difference": "Verschil", + "@difference": {}, + "percentEnergy": "Procent energie", + "@percentEnergy": {}, + "gPerBodyKg": "g per lichaams kg", + "@gPerBodyKg": { + "description": "Label used for total sums of e.g. calories or similar in grams per Kg of body weight" + }, + "total": "Totaal", + "@total": { + "description": "Label used for total sums of e.g. calories or similar" + }, + "kcal": "kcal", + "@kcal": { + "description": "Energy in a meal in kilocalories, kcal" + }, + "kcalValue": "{value} kcal", + "@kcalValue": { + "description": "A value in kcal, e.g. 500 kcal", + "type": "text", + "placeholders": { + "value": { + "type": "String" + } + } + }, + "kJ": "kJ", + "@kJ": { + "description": "Energy in a meal in kilo joules, kJ" + }, + "g": "g", + "@g": { + "description": "Abbreviation for gram" + }, + "gValue": "{value} g", + "@gValue": { + "description": "A value in grams, e.g. 5 g", + "type": "text", + "placeholders": { + "value": { + "type": "String" + } + } + }, + "percentValue": "{value} %", + "@percentValue": { + "description": "A value in percent, e.g. 10 %", + "type": "text", + "placeholders": { + "value": { + "type": "String" + } + } + }, + "protein": "Proteïne", + "@protein": {}, + "proteinShort": "P", + "@proteinShort": { + "description": "The first letter or short name of the word 'Protein', used in overviews" + }, + "carbohydrates": "Koolhydraten", + "@carbohydrates": {}, + "carbohydratesShort": "K", + "@carbohydratesShort": { + "description": "The first letter or short name of the word 'Carbohydrates', used in overviews" + }, + "sugars": "Suikers", + "@sugars": {}, + "fat": "Vet", + "@fat": {}, + "fatShort": "V", + "@fatShort": { + "description": "The first letter or short name of the word 'Fat', used in overviews" + }, + "saturatedFat": "Verzadigd vet", + "@saturatedFat": {}, + "fiber": "Vezels", + "@fiber": {}, + "sodium": "Natrium", + "@sodium": {}, + "amount": "Hoeveelheid", + "@amount": { + "description": "The amount (e.g. in grams) of an ingredient in a meal" + }, + "unit": "Eenheid", + "@unit": { + "description": "The unit used for a repetition (kg, time, etc.)" + }, + "newEntry": "Nieuwe invoer", + "@newEntry": { + "description": "Title when adding a new entry such as a weight or log entry" + }, + "noWeightEntries": "Je hebt geen gewichtsinvoer", + "@noWeightEntries": { + "description": "Message shown when the user has no logged weight entries" + }, + "noMeasurementEntries": "U heeft geen meetgegevens ingevoerd", + "@noMeasurementEntries": {}, + "moreMeasurementEntries": "Nieuwe meting toevoegen", + "@moreMeasurementEntries": { + "description": "Message shown when the user wants to add new measurement" + }, + "edit": "Wijzig", + "@edit": {}, + "loadingText": "Laden...", + "@loadingText": { + "description": "Text to show when entries are being loaded in the background: Loading..." + }, + "delete": "Verwijder", + "@delete": {}, + "confirmDelete": "Weet je zeker dat je '{toDelete}' wilt verwijderen?", + "@confirmDelete": { + "description": "Confirmation text before the user deletes an object", + "type": "text", + "placeholders": { + "toDelete": { + "type": "String" + } + } + }, + "newNutritionalPlan": "Nieuw voedingsplan", + "@newNutritionalPlan": {}, + "overview": "Overzicht", + "@overview": {}, + "toggleDetails": "Schakel details in", + "@toggleDetails": { + "description": "Switch to toggle detail / overview" + } } From 9fc0a68937f285d403b0936efbce9f8f6ceabd13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 00:07:55 +0000 Subject: [PATCH 15/54] Bump actions/upload-artifact from 5 to 6 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build-android.yml | 4 ++-- .github/workflows/build-apple.yml | 6 +++--- .github/workflows/build-linux.yml | 2 +- .github/workflows/build-windows.yml | 2 +- .github/workflows/screenshots.yml | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 4d9debf5..c72d8e6f 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -36,7 +36,7 @@ jobs: - name: Build APK run: flutter build apk --release - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-apk path: build/app/outputs/flutter-apk/app-release.apk @@ -67,7 +67,7 @@ jobs: - name: Build AAB run: flutter build appbundle --release - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-aab path: build/app/outputs/bundle/release/app-release.aab \ No newline at end of file diff --git a/.github/workflows/build-apple.yml b/.github/workflows/build-apple.yml index 4a2069d5..083677e1 100644 --- a/.github/workflows/build-apple.yml +++ b/.github/workflows/build-apple.yml @@ -31,7 +31,7 @@ jobs: cd build/ios/iphoneos zip -r Runner.app.zip Runner.app - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-ios path: build/ios/iphoneos/Runner.app.zip @@ -61,7 +61,7 @@ jobs: cd build/ios/archive zip -r Runner.xcarchive.zip Runner.xcarchive - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-ipa path: build/ios/archive/Runner.xcarchive.zip @@ -84,7 +84,7 @@ jobs: cd build/macos/Build/Products/Release zip -r wger.app.zip wger.app - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-macos path: build/macos/Build/Products/Release/wger.app.zip \ No newline at end of file diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index cb9f17b9..989036b0 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -39,7 +39,7 @@ jobs: sudo apt install -y pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev --no-install-recommends flutter build linux --release tar -zcvf linux-${{ matrix.platform }}.tar.gz build/linux/${{ matrix.platform }}/release/bundle - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-linux path: | diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 45a48257..56eb0947 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -25,7 +25,7 @@ jobs: - name: Build .exe run: flutter build windows --release - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: builds-windows path: build\windows\x64\runner\Release\wger.exe \ No newline at end of file diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 61d7662b..5f8dd2c8 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -32,7 +32,7 @@ jobs: flutter drive --driver=test_driver/screenshot_driver.dart --target=integration_test/make_screenshots_test.dart --dart-define=DEVICE_TYPE=iOSPhoneBig -d "$SIMULATOR" - name: Upload screenshots - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: screenshots-ios path: fastlane/metadata/ios/**/images/iPhone 6.9/*.png @@ -113,7 +113,7 @@ jobs: flutter drive --driver=test_driver/screenshot_driver.dart --target=integration_test/make_screenshots_test.dart --dart-define=DEVICE_TYPE=${{ matrix.device.device_type }} - name: Upload screenshots - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: screenshots-android-${{ matrix.device.folder }} path: fastlane/metadata/android/**/images/${{ matrix.device.folder }}/*.png From db78f5baf70041ed00d2a218e477582b8b71913c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 00:08:01 +0000 Subject: [PATCH 16/54] Bump actions/download-artifact from 6 to 7 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/make-release.yml | 6 +++--- .github/workflows/screenshots.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml index 0eb83b71..9afec0a3 100644 --- a/.github/workflows/make-release.yml +++ b/.github/workflows/make-release.yml @@ -67,7 +67,7 @@ jobs: ref: ${{ github.event.inputs.version }} - name: Download builds - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: path: /tmp/ @@ -115,7 +115,7 @@ jobs: # uses: ./.github/actions/flutter-common # # - name: Download builds - # uses: actions/download-artifact@v6 + # uses: actions/download-artifact@v7 # with: # path: /tmp/ # @@ -134,7 +134,7 @@ jobs: steps: - name: Download builds - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 - name: Make Github release uses: softprops/action-gh-release@v2 diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 61d7662b..e55461bc 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -129,7 +129,7 @@ jobs: - uses: actions/checkout@v6 - name: Download all screenshot artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: path: screenshots From 51ba0934b815f397ad6a5c08213625cfc68b41e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 00:03:44 +0000 Subject: [PATCH 17/54] Bump cider from 0.2.8 to 0.2.9 Bumps [cider](https://github.com/f3ath/cider) from 0.2.8 to 0.2.9. - [Release notes](https://github.com/f3ath/cider/releases) - [Changelog](https://github.com/f3ath/cider/blob/master/CHANGELOG.md) - [Commits](https://github.com/f3ath/cider/compare/0.2.8...0.2.9) --- updated-dependencies: - dependency-name: cider dependency-version: 0.2.9 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 3a7bf2ed..8852bffb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -197,10 +197,10 @@ packages: dependency: "direct dev" description: name: cider - sha256: dfff70e9324f99e315857c596c31f54cb7380cfa20dfdfdca11a3631e05b7d3e + sha256: "455e3549bd1d21708326985702703345245acd3d7a2ac485de4183affb414a2c" url: "https://pub.dev" source: hosted - version: "0.2.8" + version: "0.2.9" cli_config: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2b70b8e1..db81e01c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,7 +75,7 @@ dev_dependencies: integration_test: sdk: flutter build_runner: ^2.10.4 - cider: ^0.2.7 + cider: ^0.2.9 drift_dev: ^2.29.0 flutter_lints: ^6.0.0 freezed: ^3.2.0 From 7494dfee242b365bfd33f33d67cc60bcd6901bfb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 00:03:54 +0000 Subject: [PATCH 18/54] Bump drift from 2.29.0 to 2.30.0 Bumps [drift](https://github.com/simolus3/drift) from 2.29.0 to 2.30.0. - [Release notes](https://github.com/simolus3/drift/releases) - [Commits](https://github.com/simolus3/drift/compare/drift-2.29.0...drift-2.30.0) --- updated-dependencies: - dependency-name: drift dependency-version: 2.30.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pubspec.lock | 8 ++++---- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 8852bffb..12156b98 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -317,18 +317,18 @@ packages: dependency: "direct main" description: name: drift - sha256: "83290a32ae006a7535c5ecf300722cb77177250d9df4ee2becc5fa8a36095114" + sha256: "3669e1b68d7bffb60192ac6ba9fd2c0306804d7a00e5879f6364c69ecde53a7f" url: "https://pub.dev" source: hosted - version: "2.29.0" + version: "2.30.0" drift_dev: dependency: "direct dev" description: name: drift_dev - sha256: "6019f827544e77524ffd5134ae0cb75dfd92ef5ef3e269872af92840c929cd43" + sha256: afe4d1d2cfce6606c86f11a6196e974a2ddbfaa992956ce61e054c9b1899c769 url: "https://pub.dev" source: hosted - version: "2.29.0" + version: "2.30.0" equatable: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index db81e01c..0e16c412 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: clock: ^1.1.2 collection: ^1.18.0 cupertino_icons: ^1.0.8 - drift: ^2.29.0 + drift: ^2.30.0 equatable: ^2.0.7 fl_chart: ^1.1.1 flex_color_scheme: ^8.3.1 From 33111b79e9b788d10233fd2e097c28f7a9ba78c1 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Tue, 16 Dec 2025 13:24:01 +0100 Subject: [PATCH 19/54] Fix bug causing the "copy to log" to not work anymore --- lib/widgets/routines/gym_mode/log_page.dart | 72 +++++++++++++++------ 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index 007dbcea..b9409780 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -51,9 +51,12 @@ class LogPage extends ConsumerStatefulWidget { } class _LogPageState extends ConsumerState { - late void Function(Log) _copyFromPastLog = (_) {}; + 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() { @@ -91,9 +94,20 @@ class _LogPageState extends ConsumerState { final setConfigData = slotEntryPage.setConfigData!; - final log = Log.fromSetConfigData(setConfigData) - ..routineId = state.routine.id! - ..iteration = state.iteration; + // 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!; // Mark done sets final decorationStyle = slotEntryPage.logDone @@ -155,8 +169,7 @@ class _LogPageState extends ConsumerState { log: log, pastLogs: state.routine.filterLogsByExercise(log.exercise.id!), onCopy: (pastLog) { - // Call the function registered by the child - _copyFromPastLog(pastLog); + _logFormKey.currentState?.copyFromPastLog(pastLog); }, setStateCallback: (fn) { setState(fn); @@ -177,7 +190,7 @@ class _LogPageState extends ConsumerState { configData: setConfigData, log: log, focusNode: focusNode, - registerCopy: (fn) => _copyFromPastLog = fn, + key: _logFormKey, ), ), ), @@ -505,8 +518,6 @@ class LogFormWidget extends ConsumerStatefulWidget { final SetConfigData configData; final Log log; final FocusNode focusNode; - // Callback used by the child to register its copy function with the parent. - final void Function(void Function(Log))? registerCopy; LogFormWidget({ super.key, @@ -514,7 +525,6 @@ class LogFormWidget extends ConsumerStatefulWidget { required this.configData, required this.log, required this.focusNode, - this.registerCopy, }); @override @@ -538,9 +548,6 @@ class _LogFormWidgetState extends ConsumerState { _repetitionsController = TextEditingController(); _weightController = TextEditingController(); - // Register the copy function with the parent - widget.registerCopy?.call(copyFromPastLog); - WidgetsBinding.instance.addPostFrameCallback((_) { _syncControllersWithWidget(); }); @@ -555,11 +562,6 @@ class _LogFormWidgetState extends ConsumerState { _log = widget.log; _syncControllersWithWidget(); } - - // If the parent replaced the registerCopy callback, register again - if (oldWidget.registerCopy != widget.registerCopy) { - widget.registerCopy?.call(copyFromPastLog); - } } void _syncControllersWithWidget() { @@ -579,7 +581,7 @@ class _LogFormWidgetState extends ConsumerState { : (widget.configData.weight != null ? numberFormat.format(widget.configData.weight) : ''); } on Exception catch (e) { // Defensive fallback: set empty strings if formatting fails - widget._logger.fine('Error syncing controllers: $e'); + widget._logger.warning('Error syncing controllers: $e'); _repetitionsController.text = ''; _weightController.text = ''; } @@ -605,8 +607,38 @@ class _LogFormWidgetState extends ConsumerState { _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; - widget._logger.finer('Setting log rir to ${_log.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(); }); } From bd943267adc47c3bd10b18607c35a544893075d6 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Tue, 16 Dec 2025 13:31:15 +0100 Subject: [PATCH 20/54] 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 ef4db6bb..5592f5d5 100644 --- a/.github/actions/flutter-common/action.yml +++ b/.github/actions/flutter-common/action.yml @@ -9,7 +9,7 @@ runs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.3 + flutter-version: 3.38.5 cache: true - name: Install Flutter dependencies From 07111a6d97036e610f62012b63acaa4f0271c031 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Tue, 16 Dec 2025 13:49:42 +0100 Subject: [PATCH 21/54] Update Gemfile.lock --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2fba95de..cf685475 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,7 +8,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1190.0) + aws-partitions (1.1194.0) aws-sdk-core (3.239.2) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -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.206.0) + aws-sdk-s3 (1.207.0) aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -164,7 +164,7 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.17.1) + json (2.18.0) jwt (2.10.2) base64 logger (1.7.0) @@ -176,7 +176,7 @@ GEM nanaimo (0.4.0) naturally (2.3.0) nkf (0.2.0) - optparse (0.8.0) + optparse (0.8.1) os (1.1.4) plist (3.7.2) public_suffix (7.0.0) From c28535c18b20b93f30e3bb411d5afe3d9ba6ab81 Mon Sep 17 00:00:00 2001 From: Github-Actions Date: Tue, 16 Dec 2025 12:58:25 +0000 Subject: [PATCH 22/54] Bump version to 1.9.3 --- 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 7d45635c..ab5aadb7 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.3 +

Bug fixes and improvements.

diff --git a/pubspec.yaml b/pubspec.yaml index 0e16c412..e70eb6c6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # - the version number is taken from the git tag vX.Y.Z # - the build number is computed by reading the last one from the play store # and increasing by one -version: 1.9.2+120 +version: 1.9.3+130 environment: sdk: '>=3.8.0 <4.0.0' From 779ca1c72e4171c9400582e63188005df4b79535 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Tue, 16 Dec 2025 14:40:27 +0100 Subject: [PATCH 23/54] Rename translation file This file contains simplified Chinese --- lib/l10n/{app_zh.arb => app_zh_Hans.arb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/l10n/{app_zh.arb => app_zh_Hans.arb} (100%) diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh_Hans.arb similarity index 100% rename from lib/l10n/app_zh.arb rename to lib/l10n/app_zh_Hans.arb From f26267a6fb2203eaed05580216cdcf28f2abf029 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Wed, 17 Dec 2025 17:18:21 +0100 Subject: [PATCH 24/54] Rename back It seems flutter *needs* a fallback without country code --- lib/l10n/{app_zh_Hans.arb => app_zh.arb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/l10n/{app_zh_Hans.arb => app_zh.arb} (100%) diff --git a/lib/l10n/app_zh_Hans.arb b/lib/l10n/app_zh.arb similarity index 100% rename from lib/l10n/app_zh_Hans.arb rename to lib/l10n/app_zh.arb From 077dcaf742f69d99c332a53a7d358ee999457368 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Wed, 17 Dec 2025 15:14:47 +0100 Subject: [PATCH 25/54] Handle HTML errors in WgerHttpException These need to be handled separately when the server encounters an error and returns HTML instead of JSON. --- lib/core/exceptions/http_exception.dart | 85 +++++++++++++++++ .../exceptions/no_result_exception.dart | 0 .../exceptions/no_such_entry_exception.dart | 0 lib/exceptions/http_exception.dart | 48 ---------- lib/helpers/errors.dart | 94 ++++++++++++------- lib/main.dart | 2 +- .../measurements/measurement_category.dart | 2 +- lib/providers/auth.dart | 17 ++-- lib/providers/base_provider.dart | 10 +- lib/providers/body_weight.dart | 4 +- lib/providers/exercises.dart | 2 +- lib/providers/measurement.dart | 4 +- lib/providers/nutrition.dart | 10 +- lib/providers/routines.dart | 4 +- lib/screens/add_exercise_screen.dart | 2 +- lib/screens/auth_screen.dart | 2 +- lib/screens/measurement_entries_screen.dart | 6 +- lib/widgets/exercises/videos.dart | 20 ++-- lib/widgets/routines/forms/day.dart | 2 +- lib/widgets/routines/forms/routine.dart | 2 +- lib/widgets/routines/forms/session.dart | 2 +- lib/widgets/routines/forms/slot.dart | 2 +- lib/widgets/routines/forms/slot_entry.dart | 2 +- lib/widgets/routines/gym_mode/log_page.dart | 2 +- test/core/http_exception_test.dart | 77 +++++++++++++++ test/exercises/contribute_exercise_test.dart | 4 +- test/exercises/exercise_provider_test.dart | 2 +- test/{utils => helpers}/errors_test.dart | 0 .../measurement_category_test.dart | 2 +- .../measurement_provider_test.dart | 10 +- test/routine/forms/session_form_test.dart | 2 +- test/routine/routine_form_test.dart | 2 +- 32 files changed, 284 insertions(+), 139 deletions(-) create mode 100644 lib/core/exceptions/http_exception.dart rename lib/{ => core}/exceptions/no_result_exception.dart (100%) rename lib/{ => core}/exceptions/no_such_entry_exception.dart (100%) delete mode 100644 lib/exceptions/http_exception.dart create mode 100644 test/core/http_exception_test.dart rename test/{utils => helpers}/errors_test.dart (100%) diff --git a/lib/core/exceptions/http_exception.dart b/lib/core/exceptions/http_exception.dart new file mode 100644 index 00000000..948b8975 --- /dev/null +++ b/lib/core/exceptions/http_exception.dart @@ -0,0 +1,85 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2020 - 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart'; + +enum ErrorType { + json, + html, + text, +} + +const HTML_ERROR_KEY = 'html_error'; + +class WgerHttpException implements Exception { + Map errors = {}; + + /// The exception type. While the majority will be json, it is possible that + /// the server will return HTML, e.g. if there has been an internal server error + /// or similar. + late ErrorType type; + + /// Custom http exception + WgerHttpException(Response response) { + type = ErrorType.json; + final dynamic responseBody = response.body; + + final contentType = response.headers[HttpHeaders.contentTypeHeader]; + if (contentType != null && contentType.contains('text/html')) { + type = ErrorType.html; + } + + if (responseBody == null) { + errors = {'unknown_error': 'An unknown error occurred, no further information available'}; + } else { + try { + if (type == ErrorType.json) { + final response = json.decode(responseBody); + errors = (response is Map ? response : {'unknown_error': response}) + .cast(); + } else if (type == ErrorType.html) { + errors = {HTML_ERROR_KEY: responseBody.toString()}; + } else { + errors = {'text_error': responseBody.toString()}; + } + } catch (e) { + errors = {'unknown_error': responseBody}; + } + } + } + + WgerHttpException.fromMap(Map map) : type = ErrorType.json { + errors = map; + } + + String get htmlError { + if (type != ErrorType.html) { + return ''; + } + + return errors[HTML_ERROR_KEY] ?? ''; + } + + @override + String toString() { + return 'WgerHttpException ($type): $errors'; + } +} diff --git a/lib/exceptions/no_result_exception.dart b/lib/core/exceptions/no_result_exception.dart similarity index 100% rename from lib/exceptions/no_result_exception.dart rename to lib/core/exceptions/no_result_exception.dart diff --git a/lib/exceptions/no_such_entry_exception.dart b/lib/core/exceptions/no_such_entry_exception.dart similarity index 100% rename from lib/exceptions/no_such_entry_exception.dart rename to lib/core/exceptions/no_such_entry_exception.dart diff --git a/lib/exceptions/http_exception.dart b/lib/exceptions/http_exception.dart deleted file mode 100644 index 06cd2915..00000000 --- a/lib/exceptions/http_exception.dart +++ /dev/null @@ -1,48 +0,0 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team - * - * wger Workout Manager is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import 'dart:convert'; - -class WgerHttpException implements Exception { - Map errors = {}; - - /// Custom http exception. - /// Expects the response body of the REST call and will try to parse it to - /// JSON. Will use the response as-is if it fails. - WgerHttpException(dynamic responseBody) { - if (responseBody == null) { - errors = {'unknown_error': 'An unknown error occurred, no further information available'}; - } else { - try { - final response = json.decode(responseBody); - errors = (response is Map ? response : {'unknown_error': response}).cast(); - } catch (e) { - errors = {'unknown_error': responseBody}; - } - } - } - - WgerHttpException.fromMap(Map map) { - errors = map; - } - - @override - String toString() { - return errors.values.toList().join(', '); - } -} diff --git a/lib/helpers/errors.dart b/lib/helpers/errors.dart index 77d03a0a..926c589d 100644 --- a/lib/helpers/errors.dart +++ b/lib/helpers/errors.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 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 @@ -22,11 +22,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/main.dart'; import 'package:wger/models/workouts/log.dart'; @@ -50,16 +51,23 @@ void showHttpExceptionErrorDialog(WgerHttpException exception, {BuildContext? co return; } - final errorList = formatApiErrors(extractErrors(exception.errors)); + final theme = Theme.of(dialogContext); showDialog( context: dialogContext, builder: (ctx) => AlertDialog( title: Text(AppLocalizations.of(ctx).anErrorOccurred), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [...errorList], + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (exception.type == ErrorType.html) + ServerHtmlError(data: exception.htmlError) + else + ...formatApiErrors(extractErrors(exception.errors)), + ], + ), ), actions: [ TextButton( @@ -145,7 +153,10 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext tilePadding: EdgeInsets.zero, title: Text(i18n.errorViewDetails), children: [ - Text(issueErrorMessage, style: const TextStyle(fontWeight: FontWeight.bold)), + Text( + issueErrorMessage, + style: const TextStyle(fontWeight: FontWeight.bold), + ), Container( alignment: Alignment.topLeft, padding: const EdgeInsets.symmetric(vertical: 8.0), @@ -237,6 +248,31 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext ); } +/// A widget to render HTML errors returned by the server +/// +/// This is a simple wrapper around the `Html` Widget, with some light changes +/// to the style. +class ServerHtmlError extends StatelessWidget { + final logger = Logger('ServerHtml'); + final String data; + + ServerHtmlError({required this.data, super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Html( + data: data, + style: { + 'h1': Style(fontSize: FontSize(theme.textTheme.bodyLarge?.fontSize ?? 15)), + 'h2': Style(fontSize: FontSize(theme.textTheme.bodyMedium?.fontSize ?? 15)), + }, + doNotRenderTheseTags: const {'a'}, + ); + } +} + class CopyToClipboardButton extends StatelessWidget { final logger = Logger('CopyToClipboardButton'); final String text; @@ -423,38 +459,26 @@ class FormHttpErrorsWidget extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( constraints: const BoxConstraints(maxHeight: 250), + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.error, width: 1), + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.all(10), child: SingleChildScrollView( child: Column( children: [ - Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error), - ...formatApiErrors( - extractErrors(exception.errors), - color: Theme.of(context).colorScheme.error, - ), - ], - ), - ), - ); - } -} - -class GeneralErrorsWidget extends StatelessWidget { - final String? title; - final List widgets; - - const GeneralErrorsWidget(this.widgets, {this.title, super.key}); - - @override - Widget build(BuildContext context) { - return Container( - constraints: const BoxConstraints(maxHeight: 250), - child: SingleChildScrollView( - child: Column( - children: [ - Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error), - ...formatTextErrors(widgets, title: title, color: Theme.of(context).colorScheme.error), + Icon(Icons.error_outline, color: theme.colorScheme.error), + if (exception.type == ErrorType.html) + ServerHtmlError(data: exception.htmlError) + else + ...formatApiErrors( + extractErrors(exception.errors), + color: theme.colorScheme.error, + ), ], ), ), diff --git a/lib/main.dart b/lib/main.dart index db8836c1..47761881 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,8 +21,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart' as riverpod; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/core/locator.dart'; -import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/models/measurements/measurement_category.dart b/lib/models/measurements/measurement_category.dart index d32925db..e5c8cdfd 100644 --- a/lib/models/measurements/measurement_category.dart +++ b/lib/models/measurements/measurement_category.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/models/measurements/measurement_entry.dart'; part 'measurement_category.g.dart'; diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index a00a292a..edb73a8d 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 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 @@ -27,7 +27,7 @@ import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:version/version.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/shared_preferences.dart'; @@ -132,7 +132,7 @@ class AuthProvider with ChangeNotifier { ); if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } return login(username, password, serverUrl, null); @@ -158,8 +158,8 @@ class AuthProvider with ChangeNotifier { }, ); - if (response.statusCode != 200) { - throw WgerHttpException(response.body); + if (response.statusCode >= 400) { + throw WgerHttpException(response); } token = apiToken; @@ -169,17 +169,18 @@ class AuthProvider with ChangeNotifier { final response = await client.post( makeUri(serverUrl, LOGIN_URL), headers: { - HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', + HttpHeaders.contentTypeHeader: 'application/json; charset=utf-8', HttpHeaders.userAgentHeader: getAppNameHeader(), }, body: json.encode({'username': username, 'password': password}), ); - final responseData = json.decode(response.body); if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } + final responseData = json.decode(response.body); + token = responseData['token']; } diff --git a/lib/providers/base_provider.dart b/lib/providers/base_provider.dart index 351efe99..dee257d7 100644 --- a/lib/providers/base_provider.dart +++ b/lib/providers/base_provider.dart @@ -21,7 +21,7 @@ import 'dart:io'; import 'package:http/http.dart' as http; import 'package:http/http.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/providers/auth.dart'; import 'package:wger/providers/helpers.dart'; @@ -66,7 +66,7 @@ class WgerBaseProvider { // Something wrong with our request if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } // Process the response @@ -104,7 +104,7 @@ class WgerBaseProvider { // Something wrong with our request if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } return json.decode(response.body); @@ -120,7 +120,7 @@ class WgerBaseProvider { // Something wrong with our request if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } return json.decode(response.body); @@ -137,7 +137,7 @@ class WgerBaseProvider { // Something wrong with our request if (response.statusCode >= 400) { - throw WgerHttpException(response.body); + throw WgerHttpException(response); } return response; } diff --git a/lib/providers/body_weight.dart b/lib/providers/body_weight.dart index dddb8cec..cc93f7c3 100644 --- a/lib/providers/body_weight.dart +++ b/lib/providers/body_weight.dart @@ -18,7 +18,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/models/body_weight/weight_entry.dart'; import 'package:wger/providers/base_provider.dart'; @@ -115,7 +115,7 @@ class BodyWeightProvider with ChangeNotifier { if (response.statusCode >= 400) { _entries.insert(existingEntryIndex, existingWeightEntry); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } } } diff --git a/lib/providers/exercises.dart b/lib/providers/exercises.dart index 5ab3d70f..50c8175c 100644 --- a/lib/providers/exercises.dart +++ b/lib/providers/exercises.dart @@ -22,9 +22,9 @@ import 'dart:convert'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/core/locator.dart'; import 'package:wger/database/exercises/exercise_database.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/models/exercises/category.dart'; diff --git a/lib/providers/measurement.dart b/lib/providers/measurement.dart index c961e938..6c0292f0 100644 --- a/lib/providers/measurement.dart +++ b/lib/providers/measurement.dart @@ -18,8 +18,8 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:wger/exceptions/http_exception.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/models/measurements/measurement_entry.dart'; diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index 11a82485..3ec50f80 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -21,10 +21,10 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/core/locator.dart'; import 'package:wger/database/ingredients/ingredients_database.dart'; -import 'package:wger/exceptions/http_exception.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/models/nutrition/ingredient_image.dart'; @@ -220,7 +220,7 @@ class NutritionPlansProvider with ChangeNotifier { if (response.statusCode >= 400) { _plans.insert(existingPlanIndex, existingPlan); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } //existingPlan = null; } @@ -263,7 +263,7 @@ class NutritionPlansProvider with ChangeNotifier { if (response.statusCode >= 400) { plan.meals.insert(mealIndex, existingMeal); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } } @@ -293,7 +293,7 @@ class NutritionPlansProvider with ChangeNotifier { if (response.statusCode >= 400) { meal.mealItems.insert(mealItemIndex, existingMealItem); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } } diff --git a/lib/providers/routines.dart b/lib/providers/routines.dart index ec8e53b9..56d163dc 100644 --- a/lib/providers/routines.dart +++ b/lib/providers/routines.dart @@ -20,7 +20,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/models/exercises/exercise.dart'; @@ -374,7 +374,7 @@ class RoutinesProvider with ChangeNotifier { if (response.statusCode >= 400) { _routines.insert(routineIndex, routine); notifyListeners(); - throw WgerHttpException(response.body); + throw WgerHttpException(response); } } diff --git a/lib/screens/add_exercise_screen.dart b/lib/screens/add_exercise_screen.dart index c91f74ef..9d94f5d2 100644 --- a/lib/screens/add_exercise_screen.dart +++ b/lib/screens/add_exercise_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/core/wide_screen_wrapper.dart'; -import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart index 902018df..aaa55e46 100644 --- a/lib/screens/auth_screen.dart +++ b/lib/screens/auth_screen.dart @@ -19,7 +19,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/screens/measurement_entries_screen.dart b/lib/screens/measurement_entries_screen.dart index 550a3238..3f30ed3e 100644 --- a/lib/screens/measurement_entries_screen.dart +++ b/lib/screens/measurement_entries_screen.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -18,8 +18,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/core/wide_screen_wrapper.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/providers/measurement.dart'; diff --git a/lib/widgets/exercises/videos.dart b/lib/widgets/exercises/videos.dart index 8f38da56..18d76ef9 100644 --- a/lib/widgets/exercises/videos.dart +++ b/lib/widgets/exercises/videos.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -20,6 +20,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; import 'package:video_player/video_player.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/models/exercises/video.dart'; @@ -33,9 +34,10 @@ class ExerciseVideoWidget extends StatefulWidget { } class _ExerciseVideoWidgetState extends State { + final logger = Logger('ExerciseVideoWidgetState'); + late VideoPlayerController _controller; bool hasError = false; - final logger = Logger('ExerciseVideoWidgetState'); @override void initState() { @@ -66,10 +68,14 @@ class _ExerciseVideoWidgetState extends State { @override Widget build(BuildContext context) { return hasError - ? const GeneralErrorsWidget( - [ - 'An error happened while loading the video. If you can, please check the application logs.', - ], + ? FormHttpErrorsWidget( + WgerHttpException.fromMap( + const { + 'error': + 'An error happened while loading the video. If you can, ' + 'please check the application logs.', + }, + ), ) : _controller.value.isInitialized ? AspectRatio( diff --git a/lib/widgets/routines/forms/day.dart b/lib/widgets/routines/forms/day.dart index a0ae3c25..6af0dc10 100644 --- a/lib/widgets/routines/forms/day.dart +++ b/lib/widgets/routines/forms/day.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/widgets/routines/forms/routine.dart b/lib/widgets/routines/forms/routine.dart index bbdc9bb2..cb6d3b01 100644 --- a/lib/widgets/routines/forms/routine.dart +++ b/lib/widgets/routines/forms/routine.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/widgets/routines/forms/session.dart b/lib/widgets/routines/forms/session.dart index 231adc73..57b7d7a2 100644 --- a/lib/widgets/routines/forms/session.dart +++ b/lib/widgets/routines/forms/session.dart @@ -20,7 +20,7 @@ import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/helpers/json.dart'; diff --git a/lib/widgets/routines/forms/slot.dart b/lib/widgets/routines/forms/slot.dart index 59ab4aa9..f392d997 100644 --- a/lib/widgets/routines/forms/slot.dart +++ b/lib/widgets/routines/forms/slot.dart @@ -18,7 +18,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/day.dart'; diff --git a/lib/widgets/routines/forms/slot_entry.dart b/lib/widgets/routines/forms/slot_entry.dart index 94ccd7f5..a9580004 100644 --- a/lib/widgets/routines/forms/slot_entry.dart +++ b/lib/widgets/routines/forms/slot_entry.dart @@ -19,7 +19,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index b9409780..d8ce9e04 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -21,7 +21,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart' as provider; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/log.dart'; diff --git a/test/core/http_exception_test.dart b/test/core/http_exception_test.dart new file mode 100644 index 00000000..26b7a754 --- /dev/null +++ b/test/core/http_exception_test.dart @@ -0,0 +1,77 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2020 - 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:wger/core/exceptions/http_exception.dart'; + +void main() { + group('WgerHttpException', () { + test('parses valid JSON response', () { + final resp = http.Response( + '{"foo":"bar"}', + 400, + headers: {HttpHeaders.contentTypeHeader: 'application/json'}, + ); + + final ex = WgerHttpException(resp); + + expect(ex.type, ErrorType.json); + expect(ex.errors['foo'], 'bar'); + expect(ex.toString(), contains('WgerHttpException')); + }); + + test('falls back on malformed JSON', () { + const body = '{"foo":'; + final resp = http.Response( + body, + 500, + headers: {HttpHeaders.contentTypeHeader: 'application/json'}, + ); + + final ex = WgerHttpException(resp); + + expect(ex.type, ErrorType.json); + expect(ex.errors['unknown_error'], body); + }); + + test('detects HTML response', () { + const body = 'Error'; + final resp = http.Response( + body, + 500, + headers: {HttpHeaders.contentTypeHeader: 'text/html; charset=utf-8'}, + ); + + final ex = WgerHttpException(resp); + + expect(ex.type, ErrorType.html); + expect(ex.htmlError, body); + }); + + test('fromMap sets errors and type', () { + final map = {'field': 'value'}; + final ex = WgerHttpException.fromMap(map); + + expect(ex.type, ErrorType.json); + expect(ex.errors, map); + }); + }); +} diff --git a/test/exercises/contribute_exercise_test.dart b/test/exercises/contribute_exercise_test.dart index a909efc0..df1c3b4b 100644 --- a/test/exercises/contribute_exercise_test.dart +++ b/test/exercises/contribute_exercise_test.dart @@ -21,7 +21,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/providers/add_exercise.dart'; import 'package:wger/providers/exercises.dart'; @@ -422,7 +422,7 @@ void main() { testWidgets('Failed submission displays error message', (WidgetTester tester) async { // Setup: Create verified user and mock failed submission setupFullVerifiedUserContext(); - final httpException = WgerHttpException({ + final httpException = WgerHttpException.fromMap({ 'name': ['This field is required'], }); when(mockAddExerciseProvider.postExerciseToServer()).thenThrow(httpException); diff --git a/test/exercises/exercise_provider_test.dart b/test/exercises/exercise_provider_test.dart index 82a9d861..f0920635 100644 --- a/test/exercises/exercise_provider_test.dart +++ b/test/exercises/exercise_provider_test.dart @@ -7,8 +7,8 @@ import 'package:mockito/mockito.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/database/exercises/exercise_database.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/models/exercises/category.dart'; diff --git a/test/utils/errors_test.dart b/test/helpers/errors_test.dart similarity index 100% rename from test/utils/errors_test.dart rename to test/helpers/errors_test.dart diff --git a/test/measurements/measurement_category_test.dart b/test/measurements/measurement_category_test.dart index 21009d0e..3e16394d 100644 --- a/test/measurements/measurement_category_test.dart +++ b/test/measurements/measurement_category_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/models/measurements/measurement_entry.dart'; diff --git a/test/measurements/measurement_provider_test.dart b/test/measurements/measurement_provider_test.dart index 799ac261..675f6c1b 100644 --- a/test/measurements/measurement_provider_test.dart +++ b/test/measurements/measurement_provider_test.dart @@ -4,8 +4,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:wger/exceptions/http_exception.dart'; -import 'package:wger/exceptions/no_such_entry_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/models/measurements/measurement_category.dart'; import 'package:wger/models/measurements/measurement_entry.dart'; @@ -271,7 +271,7 @@ void main() { 'should re-add the "removed" MeasurementCategory and relay the exception on WgerHttpException', () { // arrange - when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException('{}')); + when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException.fromMap({})); // act & assert expect( @@ -330,7 +330,7 @@ void main() { test('should keep categories list as is on WgerHttpException', () { // arrange - when(mockWgerBaseProvider.patch(any, any)).thenThrow(WgerHttpException('{}')); + when(mockWgerBaseProvider.patch(any, any)).thenThrow(WgerHttpException.fromMap({})); // act & assert expect( @@ -550,7 +550,7 @@ void main() { ), const MeasurementCategory(id: 2, name: 'Biceps', unit: 'cm'), ]; - when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException('{}')); + when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException.fromMap({})); // act & assert expect( diff --git a/test/routine/forms/session_form_test.dart b/test/routine/forms/session_form_test.dart index 53302ea9..53febec2 100644 --- a/test/routine/forms/session_form_test.dart +++ b/test/routine/forms/session_form_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/session.dart'; import 'package:wger/providers/routines.dart'; diff --git a/test/routine/routine_form_test.dart b/test/routine/routine_form_test.dart index 8f287f09..c40c7426 100644 --- a/test/routine/routine_form_test.dart +++ b/test/routine/routine_form_test.dart @@ -21,7 +21,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; -import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/core/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/routine.dart'; From d187324a25601b18842f978b65ee05d29353af70 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Thu, 18 Dec 2025 10:58:40 +0100 Subject: [PATCH 26/54] Properly handle null values Basically all the fields can be nullable, so we need to set them if we want to avoid LateInitialisation errors. --- .../ingredients/ingredients_database.g.dart | 23 +- lib/models/workouts/log.dart | 32 +- lib/models/workouts/slot_data.dart | 6 +- lib/models/workouts/slot_data.g.dart | 22 +- lib/providers/gym_state.dart | 20 +- lib/providers/gym_state.g.dart | 20 +- test/core/validators_test.mocks.dart | 173 +++ .../plate_calculator_test.mocks.dart | 19 + test/user/provider_test.mocks.dart | 27 +- .../routines/gym_mode/log_page_test.dart | 139 +++ .../gym_mode/log_page_test.mocks.dart | 1069 +++++++++++++++++ 11 files changed, 1518 insertions(+), 32 deletions(-) create mode 100644 test/widgets/routines/gym_mode/log_page_test.dart create mode 100644 test/widgets/routines/gym_mode/log_page_test.mocks.dart diff --git a/lib/database/ingredients/ingredients_database.g.dart b/lib/database/ingredients/ingredients_database.g.dart index 7dc02ac3..4b73341b 100644 --- a/lib/database/ingredients/ingredients_database.g.dart +++ b/lib/database/ingredients/ingredients_database.g.dart @@ -1,3 +1,21 @@ +/* + * 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'; @@ -429,7 +447,10 @@ typedef $$IngredientsTableProcessedTableManager = $$IngredientsTableAnnotationComposer, $$IngredientsTableCreateCompanionBuilder, $$IngredientsTableUpdateCompanionBuilder, - (IngredientTable, BaseReferences<_$IngredientDatabase, $IngredientsTable, IngredientTable>), + ( + IngredientTable, + BaseReferences<_$IngredientDatabase, $IngredientsTable, IngredientTable>, + ), IngredientTable, PrefetchHooks Function() >; diff --git a/lib/models/workouts/log.dart b/lib/models/workouts/log.dart index 0c192928..f4473aa1 100644 --- a/lib/models/workouts/log.dart +++ b/lib/models/workouts/log.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 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 @@ -111,31 +111,23 @@ class Log { Log.empty(); - Log.fromSetConfigData(SetConfigData data) { + Log.fromSetConfigData(SetConfigData setConfig) { date = DateTime.now(); sessionId = null; - slotEntryId = data.slotEntryId; - exerciseBase = data.exercise; + slotEntryId = setConfig.slotEntryId; + exerciseBase = setConfig.exercise; - if (data.weight != null) { - weight = data.weight; - weightTarget = data.weight; - } - if (data.weightUnit != null) { - weightUnit = data.weightUnit; - } + weight = setConfig.weight; + weightTarget = setConfig.weight; + weightUnit = setConfig.weightUnit; - if (data.repetitions != null) { - repetitions = data.repetitions; - repetitionsTarget = data.repetitions; - } - if (data.repetitionsUnit != null) { - repetitionUnit = data.repetitionsUnit; - } + repetitions = setConfig.repetitions; + repetitionsTarget = setConfig.repetitions; + repetitionUnit = setConfig.repetitionsUnit; - rir = data.rir; - rirTarget = data.rir; + rir = setConfig.rir; + rirTarget = setConfig.rir; } // Boilerplate diff --git a/lib/models/workouts/slot_data.dart b/lib/models/workouts/slot_data.dart index cd36464e..9ef73f35 100644 --- a/lib/models/workouts/slot_data.dart +++ b/lib/models/workouts/slot_data.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -36,8 +36,8 @@ class SlotData { late List setConfigs; SlotData({ - required this.comment, - required this.isSuperset, + this.comment = '', + this.isSuperset = false, this.exerciseIds = const [], this.setConfigs = const [], }); diff --git a/lib/models/workouts/slot_data.g.dart b/lib/models/workouts/slot_data.g.dart index a8d70f14..589b2b99 100644 --- a/lib/models/workouts/slot_data.g.dart +++ b/lib/models/workouts/slot_data.g.dart @@ -1,3 +1,21 @@ +/* + * 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'; @@ -12,8 +30,8 @@ SlotData _$SlotDataFromJson(Map json) { requiredKeys: const ['comment', 'is_superset', 'exercises', 'sets'], ); return SlotData( - comment: json['comment'] as String, - isSuperset: json['is_superset'] as bool, + comment: json['comment'] as String? ?? '', + isSuperset: json['is_superset'] as bool? ?? false, exerciseIds: (json['exercises'] as List?)?.map((e) => (e as num).toInt()).toList() ?? const [], setConfigs: diff --git a/lib/providers/gym_state.dart b/lib/providers/gym_state.dart index cec04d8f..949a753f 100644 --- a/lib/providers/gym_state.dart +++ b/lib/providers/gym_state.dart @@ -1,3 +1,21 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 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 . + */ + import 'package:clock/clock.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -463,7 +481,7 @@ class GymStateNotifier extends _$GymStateNotifier { pages.add(PageEntry(type: PageType.workoutSummary, pageIndex: pageIndex + 1)); state = state.copyWith(pages: pages); - // _logger.finer(readPageStructure()); + print(readPageStructure()); _logger.finer('Initialized ${state.pages.length} pages'); } diff --git a/lib/providers/gym_state.g.dart b/lib/providers/gym_state.g.dart index 3a858e5e..7596fa4b 100644 --- a/lib/providers/gym_state.g.dart +++ b/lib/providers/gym_state.g.dart @@ -1,3 +1,21 @@ +/* + * 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'; @@ -40,7 +58,7 @@ final class GymStateNotifierProvider extends $NotifierProvider r'449bd80d3b534f68af4f0dbb8556c7f093f3b918'; +String _$gymStateNotifierHash() => r'4e1ac85de3c9f5c7dad4b0c5e6ad80ad36397610'; abstract class _$GymStateNotifier extends $Notifier { GymModeState build(); diff --git a/test/core/validators_test.mocks.dart b/test/core/validators_test.mocks.dart index f78fcd69..8791d455 100644 --- a/test/core/validators_test.mocks.dart +++ b/test/core/validators_test.mocks.dart @@ -1,3 +1,21 @@ +/* + * 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. @@ -20,6 +38,7 @@ import 'package:wger/l10n/generated/app_localizations.dart' as _i2; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member /// A class which mocks [AppLocalizations]. /// @@ -919,6 +938,39 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String get impressionGood => + (super.noSuchMethod( + Invocation.getter(#impressionGood), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#impressionGood), + ), + ) + as String); + + @override + String get impressionNeutral => + (super.noSuchMethod( + Invocation.getter(#impressionNeutral), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#impressionNeutral), + ), + ) + as String); + + @override + String get impressionBad => + (super.noSuchMethod( + Invocation.getter(#impressionBad), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#impressionBad), + ), + ) + as String); + @override String get impression => (super.noSuchMethod( @@ -1095,6 +1147,105 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String get gymModeTimerType => + (super.noSuchMethod( + Invocation.getter(#gymModeTimerType), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#gymModeTimerType), + ), + ) + as String); + + @override + String get gymModeTimerTypeHelText => + (super.noSuchMethod( + Invocation.getter(#gymModeTimerTypeHelText), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#gymModeTimerTypeHelText), + ), + ) + as String); + + @override + String get countdown => + (super.noSuchMethod( + Invocation.getter(#countdown), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#countdown), + ), + ) + as String); + + @override + String get stopwatch => + (super.noSuchMethod( + Invocation.getter(#stopwatch), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#stopwatch), + ), + ) + as String); + + @override + String get gymModeDefaultCountdownTime => + (super.noSuchMethod( + Invocation.getter(#gymModeDefaultCountdownTime), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#gymModeDefaultCountdownTime), + ), + ) + as String); + + @override + String get gymModeNotifyOnCountdownFinish => + (super.noSuchMethod( + Invocation.getter(#gymModeNotifyOnCountdownFinish), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#gymModeNotifyOnCountdownFinish), + ), + ) + as String); + + @override + String get duration => + (super.noSuchMethod( + Invocation.getter(#duration), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#duration), + ), + ) + as String); + + @override + String get volume => + (super.noSuchMethod( + Invocation.getter(#volume), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#volume), + ), + ) + as String); + + @override + String get workoutCompleted => + (super.noSuchMethod( + Invocation.getter(#workoutCompleted), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#workoutCompleted), + ), + ) + as String); + @override String get plateCalculator => (super.noSuchMethod( @@ -3677,6 +3828,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String durationHoursMinutes(int? hours, int? minutes) => + (super.noSuchMethod( + Invocation.method(#durationHoursMinutes, [hours, minutes]), + returnValue: _i3.dummyValue( + this, + Invocation.method(#durationHoursMinutes, [hours, minutes]), + ), + ) + as String); + @override String chartAllTimeTitle(String? name) => (super.noSuchMethod( @@ -3765,6 +3927,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String formMinMaxValues(int? min, int? max) => + (super.noSuchMethod( + Invocation.method(#formMinMaxValues, [min, max]), + returnValue: _i3.dummyValue( + this, + Invocation.method(#formMinMaxValues, [min, max]), + ), + ) + as String); + @override String enterMinCharacters(String? min) => (super.noSuchMethod( diff --git a/test/providers/plate_calculator_test.mocks.dart b/test/providers/plate_calculator_test.mocks.dart index bcba37ab..1036371d 100644 --- a/test/providers/plate_calculator_test.mocks.dart +++ b/test/providers/plate_calculator_test.mocks.dart @@ -1,3 +1,21 @@ +/* + * 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. @@ -21,6 +39,7 @@ import 'package:shared_preferences/src/shared_preferences_async.dart' as _i2; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member /// A class which mocks [SharedPreferencesAsync]. /// diff --git a/test/user/provider_test.mocks.dart b/test/user/provider_test.mocks.dart index c0b363ab..b8fce543 100644 --- a/test/user/provider_test.mocks.dart +++ b/test/user/provider_test.mocks.dart @@ -1,3 +1,21 @@ +/* + * 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. @@ -23,6 +41,7 @@ import 'package:wger/providers/base_provider.dart' as _i4; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member class _FakeAuthProvider_0 extends _i1.SmartFake implements _i2.AuthProvider { _FakeAuthProvider_0(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); @@ -65,14 +84,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as _i3.Client); @override - set auth(_i2.AuthProvider? _auth) => super.noSuchMethod( - Invocation.setter(#auth, _auth), + set auth(_i2.AuthProvider? value) => super.noSuchMethod( + Invocation.setter(#auth, value), returnValueForMissingStub: null, ); @override - set client(_i3.Client? _client) => super.noSuchMethod( - Invocation.setter(#client, _client), + set client(_i3.Client? value) => super.noSuchMethod( + Invocation.setter(#client, value), returnValueForMissingStub: null, ); diff --git a/test/widgets/routines/gym_mode/log_page_test.dart b/test/widgets/routines/gym_mode/log_page_test.dart new file mode 100644 index 00000000..b3fa5c8b --- /dev/null +++ b/test/widgets/routines/gym_mode/log_page_test.dart @@ -0,0 +1,139 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 - 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 . + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:provider/provider.dart' as provider; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/models/exercises/exercise.dart'; +import 'package:wger/models/workouts/day_data.dart'; +import 'package:wger/models/workouts/set_config_data.dart'; +import 'package:wger/models/workouts/slot_data.dart'; +import 'package:wger/providers/exercises.dart'; +import 'package:wger/providers/gym_state.dart'; +import 'package:wger/providers/routines.dart'; +import 'package:wger/widgets/routines/gym_mode/log_page.dart'; + +import '../../../../test_data/exercises.dart'; +import '../../../../test_data/routines.dart' as testdata; +import 'log_page_test.mocks.dart'; + +@GenerateMocks([ExercisesProvider, RoutinesProvider]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('LogPage smoke tests', () { + late List testExercises; + late ProviderContainer container; + + setUp(() { + SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); + testExercises = getTestExercises(); + container = ProviderContainer.test(); + }); + + Future pumpLogPage(WidgetTester tester) async { + await tester.pumpWidget( + UncontrolledProviderScope( + container: container, + child: provider.ChangeNotifierProvider.value( + value: MockRoutinesProvider(), + child: MaterialApp( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: LogPage(PageController()), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + } + + testWidgets('handles null values', (tester) async { + // Arrange + final notifier = container.read(gymStateProvider.notifier); + final routine = testdata.getTestRoutine(); + routine.dayDataGym = [ + DayData( + iteration: 1, + date: DateTime(2024, 11, 01), + label: '', + day: routine.dayDataGym.first.day, + slots: [ + SlotData( + isSuperset: false, + exerciseIds: [testExercises[0].id!], + setConfigs: [ + SetConfigData( + exerciseId: testExercises[0].id!, + exercise: testExercises[0], + slotEntryId: 1, + nrOfSets: 1, + repetitions: null, + repetitionsUnit: null, + weight: null, + weightUnit: null, + restTime: 120, + rir: 1.5, + rpe: 8, + textRepr: '3x100kg', + ), + ], + ), + ], + ), + ]; + notifier.state = notifier.state.copyWith( + dayId: routine.days.first.id, + routine: routine, + iteration: 1, + currentPage: 2, + ); + + // Act + notifier.calculatePages(); + + // Assert + expect(notifier.state.getSlotEntryPageByIndex()!.type, SlotPageType.log); + await pumpLogPage(tester); + expect(find.byType(LogPage), findsOneWidget); + }); + + testWidgets('renders without crashing for default slotEntryPage', (tester) async { + final notifier = container.read(gymStateProvider.notifier); + final routine = testdata.getTestRoutine(); + notifier.state = notifier.state.copyWith( + dayId: routine.days.first.id, + routine: routine, + iteration: 1, + ); + notifier.calculatePages(); + await pumpLogPage(tester); + + expect(find.byType(LogPage), findsOneWidget); + }); + }); +} diff --git a/test/widgets/routines/gym_mode/log_page_test.mocks.dart b/test/widgets/routines/gym_mode/log_page_test.mocks.dart new file mode 100644 index 00000000..500c61d8 --- /dev/null +++ b/test/widgets/routines/gym_mode/log_page_test.mocks.dart @@ -0,0 +1,1069 @@ +/* + * 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. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i19; +import 'dart:ui' as _i20; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i23; +import 'package:wger/database/exercises/exercise_database.dart' as _i3; +import 'package:wger/models/exercises/category.dart' as _i5; +import 'package:wger/models/exercises/equipment.dart' as _i6; +import 'package:wger/models/exercises/exercise.dart' as _i4; +import 'package:wger/models/exercises/language.dart' as _i8; +import 'package:wger/models/exercises/muscle.dart' as _i7; +import 'package:wger/models/workouts/base_config.dart' as _i15; +import 'package:wger/models/workouts/day.dart' as _i12; +import 'package:wger/models/workouts/day_data.dart' as _i22; +import 'package:wger/models/workouts/log.dart' as _i17; +import 'package:wger/models/workouts/repetition_unit.dart' as _i10; +import 'package:wger/models/workouts/routine.dart' as _i11; +import 'package:wger/models/workouts/session.dart' as _i16; +import 'package:wger/models/workouts/slot.dart' as _i13; +import 'package:wger/models/workouts/slot_entry.dart' as _i14; +import 'package:wger/models/workouts/weight_unit.dart' as _i9; +import 'package:wger/providers/base_provider.dart' as _i2; +import 'package:wger/providers/exercises.dart' as _i18; +import 'package:wger/providers/routines.dart' as _i21; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member + +class _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider { + _FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeExerciseDatabase_1 extends _i1.SmartFake implements _i3.ExerciseDatabase { + _FakeExerciseDatabase_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeExercise_2 extends _i1.SmartFake implements _i4.Exercise { + _FakeExercise_2(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeExerciseCategory_3 extends _i1.SmartFake implements _i5.ExerciseCategory { + _FakeExerciseCategory_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeEquipment_4 extends _i1.SmartFake implements _i6.Equipment { + _FakeEquipment_4(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeMuscle_5 extends _i1.SmartFake implements _i7.Muscle { + _FakeMuscle_5(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeLanguage_6 extends _i1.SmartFake implements _i8.Language { + _FakeLanguage_6(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeWeightUnit_7 extends _i1.SmartFake implements _i9.WeightUnit { + _FakeWeightUnit_7(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeRepetitionUnit_8 extends _i1.SmartFake implements _i10.RepetitionUnit { + _FakeRepetitionUnit_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeRoutine_9 extends _i1.SmartFake implements _i11.Routine { + _FakeRoutine_9(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeDay_10 extends _i1.SmartFake implements _i12.Day { + _FakeDay_10(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeSlot_11 extends _i1.SmartFake implements _i13.Slot { + _FakeSlot_11(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeSlotEntry_12 extends _i1.SmartFake implements _i14.SlotEntry { + _FakeSlotEntry_12(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeBaseConfig_13 extends _i1.SmartFake implements _i15.BaseConfig { + _FakeBaseConfig_13(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeWorkoutSession_14 extends _i1.SmartFake implements _i16.WorkoutSession { + _FakeWorkoutSession_14(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeLog_15 extends _i1.SmartFake implements _i17.Log { + _FakeLog_15(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +/// A class which mocks [ExercisesProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockExercisesProvider extends _i1.Mock implements _i18.ExercisesProvider { + MockExercisesProvider() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WgerBaseProvider get baseProvider => + (super.noSuchMethod( + Invocation.getter(#baseProvider), + returnValue: _FakeWgerBaseProvider_0( + this, + Invocation.getter(#baseProvider), + ), + ) + as _i2.WgerBaseProvider); + + @override + _i3.ExerciseDatabase get database => + (super.noSuchMethod( + Invocation.getter(#database), + returnValue: _FakeExerciseDatabase_1( + this, + Invocation.getter(#database), + ), + ) + as _i3.ExerciseDatabase); + + @override + List<_i4.Exercise> get exercises => + (super.noSuchMethod( + Invocation.getter(#exercises), + returnValue: <_i4.Exercise>[], + ) + as List<_i4.Exercise>); + + @override + List<_i4.Exercise> get filteredExercises => + (super.noSuchMethod( + Invocation.getter(#filteredExercises), + returnValue: <_i4.Exercise>[], + ) + as List<_i4.Exercise>); + + @override + Map> get exerciseByVariation => + (super.noSuchMethod( + Invocation.getter(#exerciseByVariation), + returnValue: >{}, + ) + as Map>); + + @override + List<_i5.ExerciseCategory> get categories => + (super.noSuchMethod( + Invocation.getter(#categories), + returnValue: <_i5.ExerciseCategory>[], + ) + as List<_i5.ExerciseCategory>); + + @override + List<_i7.Muscle> get muscles => + (super.noSuchMethod( + Invocation.getter(#muscles), + returnValue: <_i7.Muscle>[], + ) + as List<_i7.Muscle>); + + @override + List<_i6.Equipment> get equipment => + (super.noSuchMethod( + Invocation.getter(#equipment), + returnValue: <_i6.Equipment>[], + ) + as List<_i6.Equipment>); + + @override + List<_i8.Language> get languages => + (super.noSuchMethod( + Invocation.getter(#languages), + returnValue: <_i8.Language>[], + ) + as List<_i8.Language>); + + @override + set database(_i3.ExerciseDatabase? value) => super.noSuchMethod( + Invocation.setter(#database, value), + returnValueForMissingStub: null, + ); + + @override + set exercises(List<_i4.Exercise>? value) => super.noSuchMethod( + Invocation.setter(#exercises, value), + returnValueForMissingStub: null, + ); + + @override + set filteredExercises(List<_i4.Exercise>? newFilteredExercises) => super.noSuchMethod( + Invocation.setter(#filteredExercises, newFilteredExercises), + returnValueForMissingStub: null, + ); + + @override + set languages(List<_i8.Language>? languages) => super.noSuchMethod( + Invocation.setter(#languages, languages), + returnValueForMissingStub: null, + ); + + @override + bool get hasListeners => + (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) as bool); + + @override + _i19.Future setFilters(_i18.Filters? newFilters) => + (super.noSuchMethod( + Invocation.method(#setFilters, [newFilters]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + void initFilters() => super.noSuchMethod( + Invocation.method(#initFilters, []), + returnValueForMissingStub: null, + ); + + @override + _i19.Future findByFilters() => + (super.noSuchMethod( + Invocation.method(#findByFilters, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + void clear() => super.noSuchMethod( + Invocation.method(#clear, []), + returnValueForMissingStub: null, + ); + + @override + _i4.Exercise findExerciseById(int? id) => + (super.noSuchMethod( + Invocation.method(#findExerciseById, [id]), + returnValue: _FakeExercise_2( + this, + Invocation.method(#findExerciseById, [id]), + ), + ) + as _i4.Exercise); + + @override + List<_i4.Exercise> findExercisesByVariationId( + int? variationId, { + int? exerciseIdToExclude, + }) => + (super.noSuchMethod( + Invocation.method( + #findExercisesByVariationId, + [variationId], + {#exerciseIdToExclude: exerciseIdToExclude}, + ), + returnValue: <_i4.Exercise>[], + ) + as List<_i4.Exercise>); + + @override + _i5.ExerciseCategory findCategoryById(int? id) => + (super.noSuchMethod( + Invocation.method(#findCategoryById, [id]), + returnValue: _FakeExerciseCategory_3( + this, + Invocation.method(#findCategoryById, [id]), + ), + ) + as _i5.ExerciseCategory); + + @override + _i6.Equipment findEquipmentById(int? id) => + (super.noSuchMethod( + Invocation.method(#findEquipmentById, [id]), + returnValue: _FakeEquipment_4( + this, + Invocation.method(#findEquipmentById, [id]), + ), + ) + as _i6.Equipment); + + @override + _i7.Muscle findMuscleById(int? id) => + (super.noSuchMethod( + Invocation.method(#findMuscleById, [id]), + returnValue: _FakeMuscle_5( + this, + Invocation.method(#findMuscleById, [id]), + ), + ) + as _i7.Muscle); + + @override + _i8.Language findLanguageById(int? id) => + (super.noSuchMethod( + Invocation.method(#findLanguageById, [id]), + returnValue: _FakeLanguage_6( + this, + Invocation.method(#findLanguageById, [id]), + ), + ) + as _i8.Language); + + @override + _i19.Future fetchAndSetCategoriesFromApi() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetCategoriesFromApi, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetMusclesFromApi() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetMusclesFromApi, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetEquipmentsFromApi() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetEquipmentsFromApi, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetLanguagesFromApi() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetLanguagesFromApi, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetAllExercises() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetAllExercises, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future<_i4.Exercise?> fetchAndSetExercise(int? exerciseId) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetExercise, [exerciseId]), + returnValue: _i19.Future<_i4.Exercise?>.value(), + ) + as _i19.Future<_i4.Exercise?>); + + @override + _i19.Future<_i4.Exercise> handleUpdateExerciseFromApi( + _i3.ExerciseDatabase? database, + int? exerciseId, + ) => + (super.noSuchMethod( + Invocation.method(#handleUpdateExerciseFromApi, [ + database, + exerciseId, + ]), + returnValue: _i19.Future<_i4.Exercise>.value( + _FakeExercise_2( + this, + Invocation.method(#handleUpdateExerciseFromApi, [ + database, + exerciseId, + ]), + ), + ), + ) + as _i19.Future<_i4.Exercise>); + + @override + _i19.Future initCacheTimesLocalPrefs({dynamic forceInit = false}) => + (super.noSuchMethod( + Invocation.method(#initCacheTimesLocalPrefs, [], { + #forceInit: forceInit, + }), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future clearAllCachesAndPrefs() => + (super.noSuchMethod( + Invocation.method(#clearAllCachesAndPrefs, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetInitialData() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetInitialData, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future setExercisesFromDatabase( + _i3.ExerciseDatabase? database, { + bool? forceDeleteCache = false, + }) => + (super.noSuchMethod( + Invocation.method( + #setExercisesFromDatabase, + [database], + {#forceDeleteCache: forceDeleteCache}, + ), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future updateExerciseCache(_i3.ExerciseDatabase? database) => + (super.noSuchMethod( + Invocation.method(#updateExerciseCache, [database]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetMuscles(_i3.ExerciseDatabase? database) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetMuscles, [database]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetCategories(_i3.ExerciseDatabase? database) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetCategories, [database]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetLanguages(_i3.ExerciseDatabase? database) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetLanguages, [database]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetEquipments(_i3.ExerciseDatabase? database) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetEquipments, [database]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future> searchExercise( + String? name, { + String? languageCode = 'en', + bool? searchEnglish = false, + }) => + (super.noSuchMethod( + Invocation.method( + #searchExercise, + [name], + {#languageCode: languageCode, #searchEnglish: searchEnglish}, + ), + returnValue: _i19.Future>.value( + <_i4.Exercise>[], + ), + ) + as _i19.Future>); + + @override + void addListener(_i20.VoidCallback? listener) => super.noSuchMethod( + Invocation.method(#addListener, [listener]), + returnValueForMissingStub: null, + ); + + @override + void removeListener(_i20.VoidCallback? listener) => super.noSuchMethod( + Invocation.method(#removeListener, [listener]), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method(#dispose, []), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method(#notifyListeners, []), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [RoutinesProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRoutinesProvider extends _i1.Mock implements _i21.RoutinesProvider { + MockRoutinesProvider() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WgerBaseProvider get baseProvider => + (super.noSuchMethod( + Invocation.getter(#baseProvider), + returnValue: _FakeWgerBaseProvider_0( + this, + Invocation.getter(#baseProvider), + ), + ) + as _i2.WgerBaseProvider); + + @override + List<_i11.Routine> get items => + (super.noSuchMethod( + Invocation.getter(#items), + returnValue: <_i11.Routine>[], + ) + as List<_i11.Routine>); + + @override + List<_i9.WeightUnit> get weightUnits => + (super.noSuchMethod( + Invocation.getter(#weightUnits), + returnValue: <_i9.WeightUnit>[], + ) + as List<_i9.WeightUnit>); + + @override + _i9.WeightUnit get defaultWeightUnit => + (super.noSuchMethod( + Invocation.getter(#defaultWeightUnit), + returnValue: _FakeWeightUnit_7( + this, + Invocation.getter(#defaultWeightUnit), + ), + ) + as _i9.WeightUnit); + + @override + List<_i10.RepetitionUnit> get repetitionUnits => + (super.noSuchMethod( + Invocation.getter(#repetitionUnits), + returnValue: <_i10.RepetitionUnit>[], + ) + as List<_i10.RepetitionUnit>); + + @override + _i10.RepetitionUnit get defaultRepetitionUnit => + (super.noSuchMethod( + Invocation.getter(#defaultRepetitionUnit), + returnValue: _FakeRepetitionUnit_8( + this, + Invocation.getter(#defaultRepetitionUnit), + ), + ) + as _i10.RepetitionUnit); + + @override + set activeRoutine(_i11.Routine? value) => super.noSuchMethod( + Invocation.setter(#activeRoutine, value), + returnValueForMissingStub: null, + ); + + @override + set weightUnits(List<_i9.WeightUnit>? weightUnits) => super.noSuchMethod( + Invocation.setter(#weightUnits, weightUnits), + returnValueForMissingStub: null, + ); + + @override + set repetitionUnits(List<_i10.RepetitionUnit>? repetitionUnits) => super.noSuchMethod( + Invocation.setter(#repetitionUnits, repetitionUnits), + returnValueForMissingStub: null, + ); + + @override + bool get hasListeners => + (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) as bool); + + @override + void clear() => super.noSuchMethod( + Invocation.method(#clear, []), + returnValueForMissingStub: null, + ); + + @override + _i9.WeightUnit findWeightUnitById(int? id) => + (super.noSuchMethod( + Invocation.method(#findWeightUnitById, [id]), + returnValue: _FakeWeightUnit_7( + this, + Invocation.method(#findWeightUnitById, [id]), + ), + ) + as _i9.WeightUnit); + + @override + _i10.RepetitionUnit findRepetitionUnitById(int? id) => + (super.noSuchMethod( + Invocation.method(#findRepetitionUnitById, [id]), + returnValue: _FakeRepetitionUnit_8( + this, + Invocation.method(#findRepetitionUnitById, [id]), + ), + ) + as _i10.RepetitionUnit); + + @override + List<_i11.Routine> getPlans() => + (super.noSuchMethod( + Invocation.method(#getPlans, []), + returnValue: <_i11.Routine>[], + ) + as List<_i11.Routine>); + + @override + _i11.Routine findById(int? id) => + (super.noSuchMethod( + Invocation.method(#findById, [id]), + returnValue: _FakeRoutine_9( + this, + Invocation.method(#findById, [id]), + ), + ) + as _i11.Routine); + + @override + int findIndexById(int? id) => + (super.noSuchMethod( + Invocation.method(#findIndexById, [id]), + returnValue: 0, + ) + as int); + + @override + _i19.Future fetchAndSetAllRoutinesFull() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetAllRoutinesFull, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetAllRoutinesSparse() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetAllRoutinesSparse, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future setExercisesAndUnits( + List<_i22.DayData>? entries, { + Map? exercises, + }) => + (super.noSuchMethod( + Invocation.method( + #setExercisesAndUnits, + [entries], + {#exercises: exercises}, + ), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future<_i11.Routine> fetchAndSetRoutineSparse(int? planId) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetRoutineSparse, [planId]), + returnValue: _i19.Future<_i11.Routine>.value( + _FakeRoutine_9( + this, + Invocation.method(#fetchAndSetRoutineSparse, [planId]), + ), + ), + ) + as _i19.Future<_i11.Routine>); + + @override + _i19.Future<_i11.Routine> fetchAndSetRoutineFull(int? routineId) => + (super.noSuchMethod( + Invocation.method(#fetchAndSetRoutineFull, [routineId]), + returnValue: _i19.Future<_i11.Routine>.value( + _FakeRoutine_9( + this, + Invocation.method(#fetchAndSetRoutineFull, [routineId]), + ), + ), + ) + as _i19.Future<_i11.Routine>); + + @override + _i19.Future<_i11.Routine> addRoutine(_i11.Routine? routine) => + (super.noSuchMethod( + Invocation.method(#addRoutine, [routine]), + returnValue: _i19.Future<_i11.Routine>.value( + _FakeRoutine_9(this, Invocation.method(#addRoutine, [routine])), + ), + ) + as _i19.Future<_i11.Routine>); + + @override + _i19.Future editRoutine(_i11.Routine? routine) => + (super.noSuchMethod( + Invocation.method(#editRoutine, [routine]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future deleteRoutine(int? id) => + (super.noSuchMethod( + Invocation.method(#deleteRoutine, [id]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetRepetitionUnits() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetRepetitionUnits, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetWeightUnits() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetWeightUnits, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future fetchAndSetUnits() => + (super.noSuchMethod( + Invocation.method(#fetchAndSetUnits, []), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future<_i12.Day> addDay(_i12.Day? day) => + (super.noSuchMethod( + Invocation.method(#addDay, [day]), + returnValue: _i19.Future<_i12.Day>.value( + _FakeDay_10(this, Invocation.method(#addDay, [day])), + ), + ) + as _i19.Future<_i12.Day>); + + @override + _i19.Future editDay(_i12.Day? day) => + (super.noSuchMethod( + Invocation.method(#editDay, [day]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future editDays(List<_i12.Day>? days) => + (super.noSuchMethod( + Invocation.method(#editDays, [days]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future deleteDay(int? dayId) => + (super.noSuchMethod( + Invocation.method(#deleteDay, [dayId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future<_i13.Slot> addSlot(_i13.Slot? slot, int? routineId) => + (super.noSuchMethod( + Invocation.method(#addSlot, [slot, routineId]), + returnValue: _i19.Future<_i13.Slot>.value( + _FakeSlot_11( + this, + Invocation.method(#addSlot, [slot, routineId]), + ), + ), + ) + as _i19.Future<_i13.Slot>); + + @override + _i19.Future deleteSlot(int? slotId, int? routineId) => + (super.noSuchMethod( + Invocation.method(#deleteSlot, [slotId, routineId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future editSlot(_i13.Slot? slot, int? routineId) => + (super.noSuchMethod( + Invocation.method(#editSlot, [slot, routineId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future editSlots(List<_i13.Slot>? slots, int? routineId) => + (super.noSuchMethod( + Invocation.method(#editSlots, [slots, routineId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future<_i14.SlotEntry> addSlotEntry( + _i14.SlotEntry? entry, + int? routineId, + ) => + (super.noSuchMethod( + Invocation.method(#addSlotEntry, [entry, routineId]), + returnValue: _i19.Future<_i14.SlotEntry>.value( + _FakeSlotEntry_12( + this, + Invocation.method(#addSlotEntry, [entry, routineId]), + ), + ), + ) + as _i19.Future<_i14.SlotEntry>); + + @override + _i19.Future deleteSlotEntry(int? id, int? routineId) => + (super.noSuchMethod( + Invocation.method(#deleteSlotEntry, [id, routineId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future editSlotEntry(_i14.SlotEntry? entry, int? routineId) => + (super.noSuchMethod( + Invocation.method(#editSlotEntry, [entry, routineId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + String getConfigUrl(_i14.ConfigType? type) => + (super.noSuchMethod( + Invocation.method(#getConfigUrl, [type]), + returnValue: _i23.dummyValue( + this, + Invocation.method(#getConfigUrl, [type]), + ), + ) + as String); + + @override + _i19.Future<_i15.BaseConfig> editConfig( + _i15.BaseConfig? config, + _i14.ConfigType? type, + ) => + (super.noSuchMethod( + Invocation.method(#editConfig, [config, type]), + returnValue: _i19.Future<_i15.BaseConfig>.value( + _FakeBaseConfig_13( + this, + Invocation.method(#editConfig, [config, type]), + ), + ), + ) + as _i19.Future<_i15.BaseConfig>); + + @override + _i19.Future<_i15.BaseConfig> addConfig( + _i15.BaseConfig? config, + _i14.ConfigType? type, + ) => + (super.noSuchMethod( + Invocation.method(#addConfig, [config, type]), + returnValue: _i19.Future<_i15.BaseConfig>.value( + _FakeBaseConfig_13( + this, + Invocation.method(#addConfig, [config, type]), + ), + ), + ) + as _i19.Future<_i15.BaseConfig>); + + @override + _i19.Future deleteConfig(int? id, _i14.ConfigType? type) => + (super.noSuchMethod( + Invocation.method(#deleteConfig, [id, type]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future handleConfig( + _i14.SlotEntry? entry, + num? value, + _i14.ConfigType? type, + ) => + (super.noSuchMethod( + Invocation.method(#handleConfig, [entry, value, type]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future> fetchSessionData() => + (super.noSuchMethod( + Invocation.method(#fetchSessionData, []), + returnValue: _i19.Future>.value( + <_i16.WorkoutSession>[], + ), + ) + as _i19.Future>); + + @override + _i19.Future<_i16.WorkoutSession> addSession( + _i16.WorkoutSession? session, + int? routineId, + ) => + (super.noSuchMethod( + Invocation.method(#addSession, [session, routineId]), + returnValue: _i19.Future<_i16.WorkoutSession>.value( + _FakeWorkoutSession_14( + this, + Invocation.method(#addSession, [session, routineId]), + ), + ), + ) + as _i19.Future<_i16.WorkoutSession>); + + @override + _i19.Future<_i16.WorkoutSession> editSession(_i16.WorkoutSession? session) => + (super.noSuchMethod( + Invocation.method(#editSession, [session]), + returnValue: _i19.Future<_i16.WorkoutSession>.value( + _FakeWorkoutSession_14( + this, + Invocation.method(#editSession, [session]), + ), + ), + ) + as _i19.Future<_i16.WorkoutSession>); + + @override + _i19.Future<_i17.Log> addLog(_i17.Log? log) => + (super.noSuchMethod( + Invocation.method(#addLog, [log]), + returnValue: _i19.Future<_i17.Log>.value( + _FakeLog_15(this, Invocation.method(#addLog, [log])), + ), + ) + as _i19.Future<_i17.Log>); + + @override + _i19.Future deleteLog(int? logId, int? routineId) => + (super.noSuchMethod( + Invocation.method(#deleteLog, [logId, routineId]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + void addListener(_i20.VoidCallback? listener) => super.noSuchMethod( + Invocation.method(#addListener, [listener]), + returnValueForMissingStub: null, + ); + + @override + void removeListener(_i20.VoidCallback? listener) => super.noSuchMethod( + Invocation.method(#removeListener, [listener]), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method(#dispose, []), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method(#notifyListeners, []), + returnValueForMissingStub: null, + ); +} From 40837ad1b329be044ecae001e56503dea3bb27e1 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Thu, 18 Dec 2025 11:13:02 +0100 Subject: [PATCH 27/54] Add tests --- lib/widgets/routines/forms/reps_unit.dart | 8 +- lib/widgets/routines/forms/rir.dart | 6 +- lib/widgets/routines/forms/weight_unit.dart | 8 +- lib/widgets/routines/gym_mode/log_page.dart | 18 ++- .../routines/gym_mode/log_page_test.dart | 107 +++++++++++++++++- 5 files changed, 129 insertions(+), 18 deletions(-) diff --git a/lib/widgets/routines/forms/reps_unit.dart b/lib/widgets/routines/forms/reps_unit.dart index 70e53c3b..f19d7e9f 100644 --- a/lib/widgets/routines/forms/reps_unit.dart +++ b/lib/widgets/routines/forms/reps_unit.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -29,7 +29,7 @@ class RepetitionUnitInputWidget extends StatefulWidget { late int? selectedRepetitionUnit; final ValueChanged onChanged; - RepetitionUnitInputWidget(initialValue, {required this.onChanged}) { + RepetitionUnitInputWidget(int? initialValue, {super.key, required this.onChanged}) { selectedRepetitionUnit = initialValue; } @@ -47,7 +47,7 @@ class _RepetitionUnitInputWidgetState extends State { : null; return DropdownButtonFormField( - value: selectedWeightUnit, + initialValue: selectedWeightUnit, decoration: InputDecoration( labelText: AppLocalizations.of(context).repetitionUnit, ), diff --git a/lib/widgets/routines/forms/rir.dart b/lib/widgets/routines/forms/rir.dart index 22c95753..da406659 100644 --- a/lib/widgets/routines/forms/rir.dart +++ b/lib/widgets/routines/forms/rir.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -30,7 +30,7 @@ class RiRInputWidget extends StatefulWidget { static const SLIDER_START = -0.5; - RiRInputWidget(this._initialValue, {required this.onChanged}) { + RiRInputWidget(this._initialValue, {super.key, required this.onChanged}) { _logger.finer('Initializing with initial value: $_initialValue'); } diff --git a/lib/widgets/routines/forms/weight_unit.dart b/lib/widgets/routines/forms/weight_unit.dart index 8afb6784..751c6e40 100644 --- a/lib/widgets/routines/forms/weight_unit.dart +++ b/lib/widgets/routines/forms/weight_unit.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -29,7 +29,7 @@ class WeightUnitInputWidget extends StatefulWidget { late int? selectedWeightUnit; final ValueChanged onChanged; - WeightUnitInputWidget(int? initialValue, {required this.onChanged}) { + WeightUnitInputWidget(int? initialValue, {super.key, required this.onChanged}) { selectedWeightUnit = initialValue; } @@ -47,7 +47,7 @@ class _WeightUnitInputWidgetState extends State { : null; return DropdownButtonFormField( - value: selectedWeightUnit, + initialValue: selectedWeightUnit, decoration: InputDecoration(labelText: AppLocalizations.of(context).weightUnit), onChanged: (WeightUnit? newValue) { setState(() { diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index d8ce9e04..70254266 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2025 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -661,6 +661,7 @@ class _LogFormWidgetState extends ConsumerState { children: [ Flexible( child: LogsRepsWidget( + key: const ValueKey('logs-reps-widget'), controller: _repetitionsController, configData: widget.configData, focusNode: widget.focusNode, @@ -673,6 +674,7 @@ class _LogFormWidgetState extends ConsumerState { const SizedBox(width: 8), Flexible( child: LogsWeightWidget( + key: const ValueKey('logs-weight-widget'), controller: _weightController, configData: widget.configData, focusNode: widget.focusNode, @@ -690,6 +692,7 @@ class _LogFormWidgetState extends ConsumerState { children: [ Flexible( child: LogsRepsWidget( + key: const ValueKey('logs-reps-widget'), controller: _repetitionsController, configData: widget.configData, focusNode: widget.focusNode, @@ -702,6 +705,7 @@ class _LogFormWidgetState extends ConsumerState { const SizedBox(width: 8), Flexible( child: RepetitionUnitInputWidget( + key: const ValueKey('repetition-unit-input-widget'), _log.repetitionsUnitId, onChanged: (v) => {}, ), @@ -715,6 +719,7 @@ class _LogFormWidgetState extends ConsumerState { children: [ Flexible( child: LogsWeightWidget( + key: const ValueKey('logs-weight-widget'), controller: _weightController, configData: widget.configData, focusNode: widget.focusNode, @@ -726,13 +731,18 @@ class _LogFormWidgetState extends ConsumerState { ), const SizedBox(width: 8), Flexible( - child: WeightUnitInputWidget(_log.weightUnitId, onChanged: (v) => {}), + child: WeightUnitInputWidget( + _log.weightUnitId, + onChanged: (v) => {}, + key: const ValueKey('weight-unit-input-widget'), + ), ), const SizedBox(width: 8), ], ), if (_detailed) RiRInputWidget( + key: const ValueKey('rir-input-widget'), _log.rir, onChanged: (value) { if (value == '') { @@ -743,6 +753,7 @@ class _LogFormWidgetState extends ConsumerState { }, ), SwitchListTile( + key: const ValueKey('units-switch'), dense: true, title: Text(i18n.setUnitsAndRir), value: _detailed, @@ -753,6 +764,7 @@ class _LogFormWidgetState extends ConsumerState { }, ), FilledButton( + key: const ValueKey('save-log-button'), onPressed: _isSaving ? null : () async { diff --git a/test/widgets/routines/gym_mode/log_page_test.dart b/test/widgets/routines/gym_mode/log_page_test.dart index b3fa5c8b..45526e7f 100644 --- a/test/widgets/routines/gym_mode/log_page_test.dart +++ b/test/widgets/routines/gym_mode/log_page_test.dart @@ -20,12 +20,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart' as provider; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/exercises/exercise.dart'; import 'package:wger/models/workouts/day_data.dart'; +import 'package:wger/models/workouts/log.dart'; import 'package:wger/models/workouts/set_config_data.dart'; import 'package:wger/models/workouts/slot_data.dart'; import 'package:wger/providers/exercises.dart'; @@ -41,7 +43,7 @@ import 'log_page_test.mocks.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('LogPage smoke tests', () { + group('LogPage tests', () { late List testExercises; late ProviderContainer container; @@ -51,18 +53,29 @@ void main() { container = ProviderContainer.test(); }); - Future pumpLogPage(WidgetTester tester) async { + Future pumpLogPage(WidgetTester tester, {RoutinesProvider? routinesProvider}) async { + final providerValue = routinesProvider ?? MockRoutinesProvider(); + await tester.pumpWidget( UncontrolledProviderScope( container: container, child: provider.ChangeNotifierProvider.value( - value: MockRoutinesProvider(), + value: providerValue, child: MaterialApp( locale: const Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: Scaffold( - body: LogPage(PageController()), + // Provide a PageView so the PageController used by LogPage is attached + body: Builder( + builder: (context) { + final controller = PageController(); + return PageView( + controller: controller, + children: [LogPage(controller)], + ); + }, + ), ), ), ), @@ -135,5 +148,91 @@ void main() { expect(find.byType(LogPage), findsOneWidget); }); + + testWidgets('copy from past log updates form fields and shows SnackBar', (tester) async { + // Arrange + final notifier = container.read(gymStateProvider.notifier); + final routine = testdata.getTestRoutine(); + notifier.state = notifier.state.copyWith( + dayId: routine.days.first.id, + routine: routine, + iteration: 1, + ); + notifier.calculatePages(); + + // Act + // Log page is at index 2 + notifier.state = notifier.state.copyWith(currentPage: 2); + expect(notifier.state.getSlotEntryPageByIndex()!.type, SlotPageType.log); + await pumpLogPage(tester); + + // Assert + final pastLogTile = find.byKey(const ValueKey('past-log-1')); + expect(pastLogTile, findsOneWidget); + await tester.tap(pastLogTile); + await tester.pumpAndSettle(); + + final editableFields = find.byType(EditableText); + expect(editableFields, findsWidgets); + + // Get controller texts + final repControllerText = tester.widget(editableFields.at(0)).controller.text; + final weightControllerText = tester + .widget(editableFields.at(1)) + .controller + .text; + + expect(repControllerText, contains('10')); + expect(weightControllerText, contains('10')); + expect(find.byType(SnackBar), findsOneWidget); + }); + + testWidgets('save button calls addLog on RoutinesProvider', (tester) async { + // Arrange + final notifier = container.read(gymStateProvider.notifier); + final routine = testdata.getTestRoutine(); + notifier.state = notifier.state.copyWith( + dayId: routine.days.first.id, + routine: routine, + iteration: 1, + ); + notifier.calculatePages(); + notifier.state = notifier.state.copyWith(currentPage: 2); + final mockRoutines = MockRoutinesProvider(); + + // Act + await pumpLogPage(tester, routinesProvider: mockRoutines); + + final editableFields = find.byType(EditableText); + expect(editableFields, findsWidgets); + + await tester.enterText(editableFields.at(0), '7'); + await tester.enterText(editableFields.at(1), '77'); + await tester.pumpAndSettle(); + + Log? capturedLog; + when(mockRoutines.addLog(any)).thenAnswer((invocation) async { + capturedLog = invocation.positionalArguments[0] as Log; + capturedLog!.id = 42; + return capturedLog!; + }); + + final saveButton = find.byKey(const ValueKey('save-log-button')); + expect(saveButton, findsOneWidget); + + await tester.tap(saveButton); + await tester.pumpAndSettle(); + + // Assert + verify(mockRoutines.addLog(any)).called(1); + expect(capturedLog, isNotNull); + expect(capturedLog!.repetitions, equals(7)); + expect(capturedLog!.weight, equals(77)); + + final currentSlotPage = notifier.state.getSlotEntryPageByIndex()!; + expect(capturedLog!.slotEntryId, equals(currentSlotPage.setConfigData!.slotEntryId)); + expect(capturedLog!.routineId, equals(notifier.state.routine.id)); + expect(capturedLog!.iteration, equals(notifier.state.iteration)); + }); }); } From 6a2158a39744238f8b8ed3415d916cb611544e22 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Thu, 18 Dec 2025 11:55:31 +0100 Subject: [PATCH 28/54] Reload the session page in the gym mode This makes sure that we have the current state and don't try to create a new session which already exists --- lib/models/workouts/session.dart | 8 +- lib/widgets/routines/forms/session.dart | 5 +- .../routines/gym_mode/session_page.dart | 103 ++++++++++++------ test/routine/gym_mode/session_page_test.dart | 12 +- 4 files changed, 88 insertions(+), 40 deletions(-) diff --git a/lib/models/workouts/session.dart b/lib/models/workouts/session.dart index c90edaf2..a96f4db8 100644 --- a/lib/models/workouts/session.dart +++ b/lib/models/workouts/session.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,9 +16,11 @@ * along with this program. If not, see . */ +import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:logging/logging.dart'; +import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/log.dart'; @@ -62,14 +64,14 @@ class WorkoutSession { this.id, this.dayId, required this.routineId, - this.impression = 2, + this.impression = DEFAULT_IMPRESSION, this.notes = '', this.timeStart, this.timeEnd, this.logs = const [], DateTime? date, }) { - this.date = date ?? DateTime.now(); + this.date = date ?? clock.now(); } Duration? get duration { diff --git a/lib/widgets/routines/forms/session.dart b/lib/widgets/routines/forms/session.dart index 57b7d7a2..b09cbbef 100644 --- a/lib/widgets/routines/forms/session.dart +++ b/lib/widgets/routines/forms/session.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -100,6 +100,7 @@ class _SessionFormState extends State { children: [ errorMessage, ToggleButtons( + key: const ValueKey('impression-toggle-buttons'), renderBorder: false, onPressed: (int index) { setState(() { diff --git a/lib/widgets/routines/gym_mode/session_page.dart b/lib/widgets/routines/gym_mode/session_page.dart index 51830cb5..f1789a87 100644 --- a/lib/widgets/routines/gym_mode/session_page.dart +++ b/lib/widgets/routines/gym_mode/session_page.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2025 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -15,59 +15,96 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ + import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:provider/provider.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/date.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/models/workouts/routine.dart'; import 'package:wger/models/workouts/session.dart'; import 'package:wger/providers/gym_state.dart'; +import 'package:wger/providers/routines.dart'; import 'package:wger/widgets/routines/forms/session.dart'; import 'package:wger/widgets/routines/gym_mode/navigation.dart'; -class SessionPage extends ConsumerWidget { +class SessionPage extends ConsumerStatefulWidget { final PageController _controller; const SessionPage(this._controller); @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(gymStateProvider); + ConsumerState createState() => _SessionPageState(); +} - final session = state.routine.sessions - .map((sessionApi) => sessionApi.session) - .firstWhere( - (session) => session.date.isSameDayAs(clock.now()), - orElse: () => WorkoutSession( - dayId: state.dayId, - routineId: state.routine.id, - impression: DEFAULT_IMPRESSION, - date: clock.now(), - timeStart: state.startTime, - timeEnd: TimeOfDay.fromDateTime(clock.now()), - ), - ); +class _SessionPageState extends ConsumerState { + late Future _initData; + late Routine _routine; + + @override + void initState() { + super.initState(); + _initData = _reloadRoutineData(); + } + + Future _reloadRoutineData() async { + final gymState = ref.read(gymStateProvider); + _routine = await context.read().fetchAndSetRoutineFull(gymState.routine.id!); + } + + @override + Widget build(BuildContext context) { + final i18n = AppLocalizations.of(context); + final gymState = ref.read(gymStateProvider); return Column( children: [ - NavigationHeader( - AppLocalizations.of(context).workoutSession, - _controller, - ), - Expanded(child: Container()), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: SessionForm( - state.routine.id, - onSaved: () => _controller.nextPage( - duration: DEFAULT_ANIMATION_DURATION, - curve: DEFAULT_ANIMATION_CURVE, - ), - session: session, + NavigationHeader(i18n.workoutSession, widget._controller), + Expanded( + child: FutureBuilder( + future: _initData, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else { + final session = _routine.sessions + .map((sessionApi) => sessionApi.session) + .firstWhere( + (s) => s.date.isSameDayAs(clock.now()), + orElse: () => WorkoutSession( + dayId: gymState.dayId, + date: clock.now(), + routineId: gymState.routine.id, + timeStart: gymState.startTime, + timeEnd: TimeOfDay.fromDateTime(clock.now()), + ), + ); + + return Column( + children: [ + Expanded(child: Container()), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: SessionForm( + gymState.routine.id, + onSaved: () => widget._controller.nextPage( + duration: DEFAULT_ANIMATION_DURATION, + curve: DEFAULT_ANIMATION_CURVE, + ), + session: session, + ), + ), + ], + ); + } + }, ), ), - NavigationFooter(_controller), + NavigationFooter(widget._controller), ], ); } diff --git a/test/routine/gym_mode/session_page_test.dart b/test/routine/gym_mode/session_page_test.dart index 63bc210c..60382d66 100644 --- a/test/routine/gym_mode/session_page_test.dart +++ b/test/routine/gym_mode/session_page_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020, wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -56,6 +56,9 @@ void main() { when(mockRoutinesProvider.editSession(any)).thenAnswer( (_) => Future.value(testRoutine.sessions[0].session), ); + when(mockRoutinesProvider.fetchAndSetRoutineFull(any)).thenAnswer( + (_) => Future.value(testRoutine), + ); }); Widget renderSessionPage({locale = 'en'}) { @@ -85,6 +88,9 @@ void main() { testWidgets('Test that data from session is loaded', (WidgetTester tester) async { withClock(Clock.fixed(DateTime(2021, 5, 1)), () async { await tester.pumpWidget(renderSessionPage()); + await tester.pumpAndSettle(); + + debugDumpApp(); expect(find.text('10:00'), findsOneWidget); expect(find.text('12:34'), findsOneWidget); expect(find.text('This is a note'), findsOneWidget); @@ -102,6 +108,7 @@ void main() { withClock(Clock.fixed(DateTime(2021, 5, 1)), () async { await tester.pumpWidget(renderSessionPage()); + await tester.pumpAndSettle(); final startTimeField = find.byKey(const ValueKey('time-start')); expect(startTimeField, findsOneWidget); @@ -123,6 +130,7 @@ void main() { // Act await tester.pumpWidget(renderSessionPage()); + await tester.pumpAndSettle(); // Assert expect(find.text('13:35'), findsOneWidget); @@ -134,11 +142,11 @@ void main() { testWidgets('Test that correct data is send to server', (WidgetTester tester) async { withClock(Clock.fixed(DateTime(2021, 5, 1)), () async { await tester.pumpWidget(renderSessionPage()); + await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('save-button'))); final captured = verify(mockRoutinesProvider.editSession(captureAny)).captured.single as WorkoutSession; - print(captured); expect(captured.id, 1); expect(captured.impression, 3); expect(captured.notes, equals('This is a note')); From 0d0a5d8e028193582a07672a166efdbe53a987ba Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Thu, 18 Dec 2025 12:20:22 +0100 Subject: [PATCH 29/54] Bump dependencies --- pubspec.lock | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 12156b98..6b29145d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,18 +125,18 @@ packages: dependency: transitive description: name: camera_android_camerax - sha256: "1f1d1ff65223c59018d58bdac5211417c2af60bcb469c9d26f928dd412eb91cf" + sha256: "474d8355961658d43f1c976e2fa1ca715505bea1adbd56df34c581aaa70ec41f" url: "https://pub.dev" source: hosted - version: "0.6.24+3" + version: "0.6.26+2" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "035b90c1e33c2efad7548f402572078f6e514d4f82be0a315cd6c6af7e855aa8" + sha256: "0efb057a1fecdbf9b697272fbf79afbd47ac0e7bd69b4d900d3f304b31d93bad" url: "https://pub.dev" source: hosted - version: "0.9.22+6" + version: "0.9.22+7" camera_platform_interface: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: camera_web - sha256: "77e53acb64d9de8917424eeb32b5c7c73572d1e00954bbf54a1e609d79a751a2" + sha256: "3bc7bb1657a0f29c34116453c5d5e528c23efcf5e75aac0a3387cf108040bf65" url: "https://pub.dev" source: hosted - version: "0.3.5+1" + version: "0.3.5+2" carousel_slider: dependency: "direct main" description: @@ -667,10 +667,10 @@ packages: dependency: transitive description: name: image - sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + sha256: "51555e36056541237b15b57afc31a0f53d4f9aefd9bd00873a6dc0090e54e332" url: "https://pub.dev" source: hosted - version: "4.5.4" + version: "4.6.0" image_picker: dependency: "direct main" description: @@ -699,10 +699,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "997d100ce1dda5b1ba4085194c5e36c9f8a1fb7987f6a36ab677a344cd2dc986" + sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" url: "https://pub.dev" source: hosted - version: "0.8.13+2" + version: "0.8.13+3" image_picker_linux: dependency: transitive description: @@ -976,10 +976,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "97390a0719146c7c3e71b6866c34f1cde92685933165c1c671984390d2aca776" + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.5.1" path_provider_linux: dependency: transitive description: @@ -1176,18 +1176,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "46a46fd64659eff15f4638bbe19de43f9483f0e0bf024a9fb6b3582064bacc7b" + sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" url: "https://pub.dev" source: hosted - version: "2.4.17" + version: "2.4.18" shared_preferences_foundation: dependency: transitive description: @@ -1333,10 +1333,10 @@ packages: dependency: transitive description: name: sqlparser - sha256: "54eea43e36dd3769274c3108625f9ea1a382f8d2ac8b16f3e4589d9bd9b0e16c" + sha256: "162435ede92bcc793ea939fdc0452eef0a73d11f8ed053b58a89792fba749da5" url: "https://pub.dev" source: hosted - version: "0.42.0" + version: "0.42.1" stack_trace: dependency: transitive description: @@ -1565,18 +1565,18 @@ packages: dependency: transitive description: name: video_player_android - sha256: "3f7ef3fb7b29f510e58f4d56b6ffbc3463b1071f2cf56e10f8d25f5b991ed85b" + sha256: "8587f7b1e1ad7a7b8f7a7e153bd6de8607168f865f0bd983ef1f92efd3f4a02c" url: "https://pub.dev" source: hosted - version: "2.8.21" + version: "2.9.0" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "6bced1739cf1f96f03058118adb8ac0dd6f96aa1a1a6e526424ab92fd2a6a77d" + sha256: e4d33b79a064498c6eb3a6a492b6a5012573d4943c28d566caf1a6c0840fe78d url: "https://pub.dev" source: hosted - version: "2.8.7" + version: "2.8.8" video_player_platform_interface: dependency: transitive description: @@ -1605,10 +1605,10 @@ packages: dependency: transitive description: name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249 url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.0" web: dependency: transitive description: 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 30/54] 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 31/54] 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 32/54] 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 33/54] 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 34/54] 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 35/54] 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 36/54] 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 37/54] 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 38/54] 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 39/54] 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 40/54] 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 41/54] 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 42/54] 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 43/54] 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 44/54] 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 45/54] 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 46/54] 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 47/54] 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 48/54] 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 49/54] 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 50/54] 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 51/54] 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 52/54] 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 53/54] 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 54/54] 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);