Merge branch 'master' into feature/exercise-crowdsourcing

# Conflicts:
#	lib/models/exercises/exercise.g.dart
#	pubspec.lock
#	pubspec.yaml
#	test/gallery/gallery_screen_test.mocks.dart
#	test/measurements/measurement_provider_test.mocks.dart
#	test/nutrition/nutritional_plan_form_test.mocks.dart
#	test/workout/workout_form_test.mocks.dart
#	test/workout/workout_set_form_test.mocks.dart
This commit is contained in:
Roland Geider
2022-07-01 09:47:52 +02:00
23 changed files with 680 additions and 217 deletions

View File

@@ -30,8 +30,7 @@ jobs:
- name: Test app
run: flutter test --coverage
#- name: Upload coverage to Codecov
# uses: codecov/codecov-action@v1
# with:
# token: ${{ secrets.CODECOV_TOKEN }}
# file: coverage/lcov.info
- name: Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,7 +1,7 @@
# Mobile app for wger Workout Manager
wger is a free, open source flutter application that manages and tracks/logs
your exercises and personal workouts, weight and diet plans. This is the mobile
wger is a free, open-source flutter application that manages and tracks/logs
your exercises and personal workouts, weight, and diet plans. This is the mobile
app written with Flutter, it talks via REST with the main server.
If you want to contribute, hop on the Discord server and say hi!
@@ -26,13 +26,13 @@ If you want to contribute, hop on the Discord server and say hi!
## Development
### 1
Install the wger server, the easiest way is starting the development docker-compose:
Install the wger server, the easiest way is to start the development docker-compose:
<https://github.com/wger-project/wger>
Alternatively, you can use one of our test servers, just ask us for access.
### 2
Install Flutter, all its dependencies and create a new virtual device:
Install Flutter, and all its dependencies, and create a new virtual device:
<https://flutter.dev/docs/get-started/install>.
The app currently uses flutter 2.10
@@ -45,10 +45,10 @@ WGER_API_KEY=123456
```
To just run/develop the app it only needs to have any value for WGER_API_KEY, but
you need a correct value if you want to register via the app. For this you need
to allow (a probably dedicated) user on the wger server to register users in its
you need a correct value if you want to register via the app. For this, you need
to allow (a probably dedicated) user on the wger server to register users on its
behalf. For this, generate an API KEY by visiting <http://localhost:8000/de/user/api-key>
on your local instance and then run ``python3 manage.py add-user-rest theusername``
on your local instance and then run ``python3 manage.py add-user-rest the username``
You can later list all the registered users with: ``python3 manage.py list-users-api``
@@ -58,10 +58,10 @@ You can later list all the registered users with: ``python3 manage.py list-users
### 4
Start the application with ``flutter run`` or use your IDE
(please note that depending on how you run your emulator you will need to change the
IP address of the server)
(please note that depending on how you run your emulator you will need to change the IP address of the server)
You can run the tests with the ``flutter test``
You can run the tests with ``flutter test``
## Translation
Translate the app to your language on [Weblate](https://hosted.weblate.org/engage/wger/).
@@ -70,7 +70,7 @@ Translate the app to your language on [Weblate](https://hosted.weblate.org/enga
## Contact
Feel free to get in touch if you found this useful or something didn't behave
Feel free to get in touch if you found this useful or if something didn't behave
as expected. We can't fix what we don't know about, so please report liberally.
If you're not sure if something is a bug or not, feel free to file a bug anyway.
@@ -83,7 +83,7 @@ If you're not sure if something is a bug or not, feel free to file a bug anyway.
The application is licensed under the GNU Affero General Public License 3 or later
(AGPL 3+) with an app store exception.
As an additional permission under section 7, you are allowed to distribute the
As additional permission under section 7, you are allowed to distribute the
software through an app store, even if that store has restrictive terms and
conditions that are incompatible with the AGPL, provided that the source is also
available under the AGPL with or without this permission through a channel without
@@ -92,3 +92,4 @@ those restrictive terms and conditions.
The initial exercise and ingredient data is licensed additionally under one of
the Creative Commons licenses, see the individual exercises for more details.

View File

@@ -1,6 +1,14 @@
# Release process
## 1. Dry-run release before uploading
## 1. Update flutter version
If we use a new version, update the version used by
* Github Actions in `android-release.yaml` in this repository
* Fdroid build recipe in [their repo](https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/de.wger.flutter.yml).
Since this can potentially take some time, it should happen well in advance
## 2. Dry-run release before uploading
* Increase build nr in pubspec.yaml
* `flutter build appbundle --release`
@@ -13,7 +21,7 @@ Also note that if a language was added over the weblate UI, it might be necessar
to set the correct language code:
<https://support.google.com/googleplay/android-developer/answer/9844778?hl=en#zippy=%2Cview-list-of-available-languages>
## 2. Push tags to trigger release
## 3. Push tags to trigger release
Make sure that the commit that will be tagged was already pushed or didn't change
any dart code, otherwise the automatic linter might push a "correction" commit
@@ -25,6 +33,6 @@ by github actions.
`TAG=vX.Y.Z && git tag $TAG && git push origin $TAG && git tag -d $TAG`
## 3. Edit release
## 4. Edit release
I necessary, edit the created release on github

View File

@@ -38,5 +38,519 @@
"date": "Дата",
"@date": {
"description": "The date of a workout log or body weight entry"
},
"usernameValidChars": "Ім'я користувача може містити лише літери, цифри й символи @, +, ., - та _",
"@usernameValidChars": {
"description": "Error message when the user tries to register a username with forbidden characters"
},
"passwordsDontMatch": "Паролі не збігаються",
"@passwordsDontMatch": {
"description": "Error message when the user enters two different passwords during registration"
},
"confirmPassword": "Підтвердьте пароль",
"@confirmPassword": {},
"email": "Адреса електронної пошти",
"@email": {},
"invalidUsername": "Будь ласка, введіть припустиме ім'я користувача",
"@invalidUsername": {
"description": "Error message when the user enters an invalid username"
},
"username": "Ім'я користувача",
"@username": {},
"customServerHint": "Введіть адресу свого власного сервера, інакше буде використовуватися сервер за промовчанням",
"@customServerHint": {
"description": "Hint text for the form where the users can enter their own wger instance"
},
"reset": "Скинути",
"@reset": {
"description": "Button text allowing the user to reset the entered values to the default"
},
"loginInstead": "Натомість увійти",
"@loginInstead": {},
"labelWorkoutPlans": "Плани тренувань",
"@labelWorkoutPlans": {
"description": "Title for screen workout plans"
},
"labelBottomNavWorkout": "Тренування",
"@labelBottomNavWorkout": {
"description": "Label used in bottom navigation, use a short word"
},
"labelWorkoutLogs": "Журнали тренувань",
"@labelWorkoutLogs": {
"description": "(Workout) logs"
},
"labelWorkoutPlan": "План тренування",
"@labelWorkoutPlan": {
"description": "Title for screen workout plan"
},
"labelDashboard": "Панель керування",
"@labelDashboard": {
"description": "Title for screen dashboard"
},
"successfullyDeleted": "Видалено",
"@successfullyDeleted": {
"description": "Message when an item was successfully deleted"
},
"exercise": "Вправа",
"@exercise": {
"description": "An exercise for a workout"
},
"searchExercise": "Пошук вправ для додавання",
"@searchExercise": {
"description": "Label on set form. Selected exercises are added to the set"
},
"supersetWith": "супер-набір з",
"@supersetWith": {
"description": "Text used between exercise cards when adding a new set. Translate as something like 'in a superset with'"
},
"equipment": "Обладнання",
"@equipment": {
"description": "Equipment needed to perform an exercise"
},
"muscles": "М'язи",
"@muscles": {
"description": "(main) muscles trained by an exercise"
},
"musclesSecondary": "Вторинні м'язи",
"@musclesSecondary": {
"description": "secondary muscles trained by an exercise"
},
"category": "Категорія",
"@category": {
"description": "Category for an exercise, ingredient, etc."
},
"newWorkout": "Новий план тренувань",
"@newWorkout": {
"description": "Header when adding a new workout"
},
"repetitions": "Повторень",
"@repetitions": {
"description": "Repetitions for an exercise set"
},
"reps": "Повтор",
"@reps": {
"description": "Shorthand for repetitions, used when space constraints are tighter"
},
"rirNotUsed": "RiR не використовується",
"@rirNotUsed": {
"description": "Label used in RiR slider when the RiR value is not used/saved for the current setting or log"
},
"repetitionUnit": "Одиниця повторень",
"@repetitionUnit": {},
"set": "Набір",
"@set": {
"description": "A set in a workout plan"
},
"setNr": "Набір {nr}",
"@setNr": {
"description": "Header in form indicating the number of the current set. Can also be translated as something like 'Set Nr. xy'.",
"type": "text",
"placeholders": {
"nr": {}
}
},
"sameRepetitions": "Якщо ви робите однакові повторення та вагу для всіх наборів, то ви можете просто заповнити один рядок. Наприклад, для 4 наборів просто введіть 10 для повторень, це автоматично стане «4 x 10».",
"@sameRepetitions": {},
"comment": "Коментар",
"@comment": {
"description": "Comment, additional information"
},
"impression": "Враження",
"@impression": {
"description": "General impression (e.g. for a workout session) such as good, bad, etc."
},
"newDay": "Новий день",
"@newDay": {},
"selectExercises": "Якщо ви хочете зробити супер-набір, ви можете пошукати декілька вправ, вони будуть згруповані разом",
"@selectExercises": {},
"gymMode": "Режим тренажерного залу",
"@gymMode": {
"description": "Label when starting the gym mode"
},
"plateCalculator": "Пластини",
"@plateCalculator": {
"description": "Label used for the plate calculator in the gym mode"
},
"plateCalculatorNotDivisible": "Неможливо досягти ваги з наявними пластинами",
"@plateCalculatorNotDivisible": {
"description": "Error message when the current weight is not reachable with plates (e.g. 33.1 kg)"
},
"pause": "Пауза",
"@pause": {
"description": "Noun, not an imperative! Label used for the pause when using the gym mode"
},
"jumpTo": "Перейти до",
"@jumpTo": {
"description": "Imperative. Label used in popup allowing the user to jump to a specific exercise while in the gym mode"
},
"todaysWorkout": "Ваше сьогоднішнє тренування",
"@todaysWorkout": {},
"password": "Пароль",
"@password": {},
"passwordTooShort": "Пароль занадто короткий",
"@passwordTooShort": {
"description": "Error message when the user a password that is too short"
},
"invalidEmail": "Будь ласка, введіть прийнятну адресу електронної пошти",
"@invalidEmail": {
"description": "Error message when the user enters an invalid email"
},
"customServerUrl": "URL-адреса екземпляра wger",
"@customServerUrl": {
"description": "Label in the form where the users can enter their own wger instance"
},
"successfullySaved": "Збережено",
"@successfullySaved": {
"description": "Message when an item was successfully saved"
},
"registerInstead": "Натомість зареєструватися",
"@registerInstead": {},
"labelBottomNavNutrition": "Харчування",
"@labelBottomNavNutrition": {
"description": "Label used in bottom navigation, use a short word"
},
"weightUnit": "Одиниця ваги",
"@weightUnit": {},
"noWorkoutPlans": "У вас немає планів тренувань",
"@noWorkoutPlans": {
"description": "Message shown when the user has no workout plans"
},
"dayDescriptionHelp": "Опис того, що робиться в цей день (наприклад, «день підтягування») або які частини тіла тренуються (наприклад, «груди та плечі»)",
"@dayDescriptionHelp": {},
"workoutSession": "Сеанс тренування",
"@workoutSession": {
"description": "A (logged) workout session"
},
"notes": "Нотатки",
"@notes": {
"description": "Personal notes, e.g. for a workout session"
},
"newSet": "Новий набір",
"@newSet": {
"description": "Header when adding a new set to a workout day"
},
"measurementCategoriesHelpText": "Категорія вимірювання, така як \"біцепс\" або \"жир тіла\"",
"@measurementCategoriesHelpText": {},
"time": "Час",
"@time": {
"description": "The time of a meal or workout"
},
"value": "Значення",
"@value": {
"description": "The value of a measurement entry"
},
"timeStart": "Час початку",
"@timeStart": {
"description": "The starting time of a workout"
},
"timeEnd": "Час закінчення",
"@timeEnd": {
"description": "The end time of a workout"
},
"kcal": "ккал",
"@kcal": {
"description": "Energy in a meal in kilocalories, kcal"
},
"fat": "Жири",
"@fat": {},
"newNutritionalPlan": "Новий план харчування",
"@newNutritionalPlan": {},
"sugars": "Цукри",
"@sugars": {},
"aboutDescription": "Дякуємо за використання wger! wger - це спільний проєкт з відкритим кодом, створений ентузіастами фітнесу з усього світу.",
"@aboutDescription": {
"description": "Text in the about dialog"
},
"aboutBugsText": "Зв'яжіться з нами, якщо щось відбувається не так, як очікувалося, або якщо є функція, яка, на вашу думку, відсутня.",
"@aboutBugsText": {
"description": "Text for bugs section in the about dialog"
},
"selectExercise": "Будь ласка, виберіть вправу",
"@selectExercise": {
"description": "Error message when the user hasn't selected an exercise in the form"
},
"calendar": "Календар",
"@calendar": {},
"enterValue": "Будь ласка, введіть значення",
"@enterValue": {
"description": "Error message when the user hasn't entered a value on a required field"
},
"selectIngredient": "Будь ласка, оберіть інгредієнт",
"@selectIngredient": {
"description": "Error message when the user hasn't selected an ingredient from the autocompleter"
},
"productFoundDescription": "Штрих-код відповідає цьому виробу: {productName}. Бажаєте продовжити?",
"@productFoundDescription": {
"description": "Dialog info when product is found with barcode",
"type": "text",
"placeholders": {
"productName": {}
}
},
"description": "Опис",
"@description": {},
"name": "Ім'я",
"@name": {
"description": "Name for a workout or nutritional plan"
},
"save": "Зберегти",
"@save": {},
"addSet": "Додати набір",
"@addSet": {
"description": "Label for the button that adds a set (to a workout day)"
},
"addMeal": "Додати страву",
"@addMeal": {},
"mealLogged": "Страву записано в щоденник",
"@mealLogged": {},
"logMeal": "Записати цю страву",
"@logMeal": {},
"addIngredient": "Додати інгредієнт",
"@addIngredient": {},
"logIngredient": "Зберегти в щоденник харчування",
"@logIngredient": {},
"searchIngredient": "Пошук інгредієнта",
"@searchIngredient": {
"description": "Label on ingredient search form"
},
"nutritionalDiary": "Щоденник харчування",
"@nutritionalDiary": {},
"nutritionalPlans": "Плани харчування",
"@nutritionalPlans": {},
"noNutritionalPlans": "Ви не маєте планів харчування",
"@noNutritionalPlans": {
"description": "Message shown when the user has no nutritional plans"
},
"anErrorOccurred": "Сталася помилка!",
"@anErrorOccurred": {},
"measurement": "Вимірювання",
"@measurement": {},
"logHelpEntries": "Якщо на один день припадає більше одного запису з однаковою кількістю повторень, але різними вагами, то на графіку буде показано тільки запис з більшою вагою.",
"@logHelpEntries": {},
"logHelpEntriesUnits": "Зверніть увагу, що на графіку відображаються тільки записи з одиницями ваги (кг або lb) і повтореннями. Інші комбінації, такі як час або \"до відмови\", тут ігноруються.",
"@logHelpEntriesUnits": {},
"measurements": "Вимірювання",
"@measurements": {
"description": "Categories for the measurements such as biceps size, body fat, etc."
},
"measurementEntriesHelpText": "Одиниця вимірювання, яка використовується для вимірювання категорії, як от \"см\" або \"%\"",
"@measurementEntriesHelpText": {},
"start": "Почати",
"@start": {
"description": "Label on button to start the gym mode (i.e., an imperative)"
},
"timeStartAhead": "Час початку не може передувати часу завершення",
"@timeStartAhead": {},
"ingredient": "Інгредієнт",
"@ingredient": {},
"energy": "Енергія",
"@energy": {
"description": "Energy in a meal, ingredient etc. e.g. in kJ"
},
"energyShort": "Е",
"@energyShort": {
"description": "The first letter or short name of the word 'Energy', used in overviews"
},
"macronutrients": "Макроелементи",
"@macronutrients": {},
"planned": "Заплановано",
"@planned": {
"description": "Header for the column of 'planned' nutritional values, i.e. what should be eaten"
},
"logged": "Записано",
"@logged": {
"description": "Header for the column of 'logged' nutritional values, i.e. what was eaten"
},
"weekAverage": "В середньому за 7 днів",
"@weekAverage": {
"description": "Header for the column of '7 day average' nutritional values, i.e. what was logged last week"
},
"percentEnergy": "Відсоток енергії",
"@percentEnergy": {},
"gPerBodyKg": "г на кг тіла",
"@gPerBodyKg": {
"description": "Label used for total sums of e.g. calories or similar in grams per Kg of body weight"
},
"total": "Усього",
"@total": {
"description": "Label used for total sums of e.g. calories or similar"
},
"kJ": "кДж",
"@kJ": {
"description": "Energy in a meal in kilo joules, kJ"
},
"g": "г",
"@g": {
"description": "Abbreviation for gram"
},
"protein": "Білок",
"@protein": {},
"proteinShort": "Б",
"@proteinShort": {
"description": "The first letter or short name of the word 'Protein', used in overviews"
},
"carbohydrates": "Вуглеводи",
"@carbohydrates": {},
"carbohydratesShort": "В",
"@carbohydratesShort": {
"description": "The first letter or short name of the word 'Carbohydrates', used in overviews"
},
"fatShort": "Ж",
"@fatShort": {
"description": "The first letter or short name of the word 'Fat', used in overviews"
},
"saturatedFat": "Насичені жири",
"@saturatedFat": {},
"fibres": "Волокна",
"@fibres": {},
"sodium": "Натрій",
"@sodium": {},
"amount": "Сума",
"@amount": {
"description": "The amount (e.g. in grams) of an ingredient in a meal"
},
"unit": "Одиниця",
"@unit": {
"description": "The unit used for a repetition (kg, time, etc.)"
},
"newEntry": "Новий запис",
"@newEntry": {
"description": "Title when adding a new entry such as a weight or log entry"
},
"noWeightEntries": "У вас немає записів ваги",
"@noWeightEntries": {
"description": "Message shown when the user has no logged weight entries"
},
"loadingText": "Завантаження...",
"@loadingText": {
"description": "Text to show when entries are being loaded in the background: Loading..."
},
"confirmDelete": "Ви впевнені, що бажаєте видалити '{toDelete}'?",
"@confirmDelete": {
"description": "Confirmation text before the user deletes an object",
"type": "text",
"placeholders": {
"toDelete": {}
}
},
"toggleDetails": "Перемкнути відомості",
"@toggleDetails": {
"description": "Switch to toggle detail / overview"
},
"goToDetailPage": "Перейти до сторінки відомостей",
"@goToDetailPage": {},
"aboutSourceTitle": "Джерельний код",
"@aboutSourceTitle": {
"description": "Title for source code section in the about dialog"
},
"aboutSourceText": "Отримайте джерельний код цього додатка та його сервера на github",
"@aboutSourceText": {
"description": "Text for source code section in the about dialog"
},
"aboutBugsTitle": "У вас є проблема чи ідея?",
"@aboutBugsTitle": {
"description": "Title for bugs section in the about dialog"
},
"aboutContactUsTitle": "Привітайтеся!",
"@aboutContactUsTitle": {
"description": "Title for contact us section in the about dialog"
},
"aboutContactUsText": "Якщо ви хочете поспілкуватися з нами, зайдіть на сервер Discord і зв'яжіться з нами",
"@aboutContactUsText": {
"description": "Text for contact us section in the about dialog"
},
"aboutTranslationTitle": "Перекладання",
"@aboutTranslationTitle": {
"description": "Title for translation section in the about dialog"
},
"aboutTranslationText": "Цей додаток перекладено на Weblate. Якщо ви також хочете допомогти, клацніть на посилання та почніть переклад",
"@aboutTranslationText": {
"description": "Text for translation section in the about dialog"
},
"goToToday": "Перейти до сьогодні",
"@goToToday": {
"description": "Label on button to jump back to 'today' in the calendar widget"
},
"enterRepetitionsOrWeight": "Будь ласка, заповніть кількість повторень або вагу принаймні для одного з наборів",
"@enterRepetitionsOrWeight": {
"description": "Error message when the user hasn't filled in the forms for exercise sets"
},
"enterCharacters": "Будь ласка, введіть між{min} та {max} символами",
"@enterCharacters": {
"description": "Error message when the user hasn't entered the correct number of characters in a form",
"type": "text",
"placeholders": {
"min": {},
"max": {}
}
},
"nrOfSets": "Наборів на вправу: {nrOfSets}",
"@nrOfSets": {
"description": "Label shown on the slider where the user selects the nr of sets",
"type": "text",
"placeholders": {
"nrOfSets": {}
}
},
"setUnitsAndRir": "Встановіть одиниці вимірювання та RiR",
"@setUnitsAndRir": {
"description": "Label shown on the slider where the user can toggle showing units and RiR",
"type": "text"
},
"enterValidNumber": "Будь ласка, введіть прийнятний номер",
"@enterValidNumber": {
"description": "Error message when the user has submitted an invalid number (e.g. '3,.,.,.')"
},
"recentlyUsedIngredients": "Останні інгредієнти",
"@recentlyUsedIngredients": {
"description": "A message when a user adds a new ingredient to a meal."
},
"selectImage": "Будь ласка, оберіть зображення",
"@selectImage": {
"description": "Label and error message when the user hasn't selected an image to save"
},
"optionsLabel": "Параметри",
"@optionsLabel": {
"description": "Label for the popup with general app options"
},
"takePicture": "Зробити знімок",
"@takePicture": {},
"chooseFromLibrary": "Вибрати з бібліотеки фотографій",
"@chooseFromLibrary": {},
"gallery": "Галерея",
"@gallery": {},
"addImage": "Додати зображення",
"@addImage": {},
"dataCopied": "Дані скопійовано до нового запису",
"@dataCopied": {
"description": "Snackbar message to show on copying data to a new log entry"
},
"appUpdateTitle": "Потрібне оновлення",
"@appUpdateTitle": {},
"appUpdateContent": "Ця версія програми несумісна з сервером, будь ласка, оновіть додаток.",
"@appUpdateContent": {},
"productFound": "Виріб знайдено",
"@productFound": {
"description": "Header label for dialog when product is found with barcode"
},
"productNotFound": "Виріб не знайдено",
"@productNotFound": {
"description": "Header label for dialog when product is not found with barcode"
},
"productNotFoundDescription": "Виріб із відсканованим штрих-кодом {barcode} не знайдено в базі даних Wger",
"@productNotFoundDescription": {
"description": "Dialog info when product is not found with barcode",
"type": "text",
"placeholders": {
"barcode": {}
}
},
"scanBarcode": "Сканувати штрих-код",
"@scanBarcode": {
"description": "Label for scan barcode button"
},
"close": "Закрити",
"@close": {
"description": "Translation for close"
}
}

View File

@@ -18,8 +18,7 @@ WeightEntry _$WeightEntryFromJson(Map<String, dynamic> json) {
);
}
Map<String, dynamic> _$WeightEntryToJson(WeightEntry instance) =>
<String, dynamic>{
Map<String, dynamic> _$WeightEntryToJson(WeightEntry instance) => <String, dynamic>{
'id': instance.id,
'weight': numToString(instance.weight),
'date': toDate(instance.date),

View File

@@ -17,8 +17,7 @@ ExerciseCategory _$ExerciseCategoryFromJson(Map<String, dynamic> json) {
);
}
Map<String, dynamic> _$ExerciseCategoryToJson(ExerciseCategory instance) =>
<String, dynamic>{
Map<String, dynamic> _$ExerciseCategoryToJson(ExerciseCategory instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
};

View File

@@ -20,8 +20,7 @@ ExerciseImage _$ExerciseImageFromJson(Map<String, dynamic> json) {
);
}
Map<String, dynamic> _$ExerciseImageToJson(ExerciseImage instance) =>
<String, dynamic>{
Map<String, dynamic> _$ExerciseImageToJson(ExerciseImage instance) => <String, dynamic>{
'id': instance.id,
'uuid': instance.uuid,
'exercise_base': instance.exerciseBaseId,

View File

@@ -22,9 +22,7 @@ MeasurementCategory _$MeasurementCategoryFromJson(Map<String, dynamic> json) {
);
}
Map<String, dynamic> _$MeasurementCategoryToJson(
MeasurementCategory instance) =>
<String, dynamic>{
Map<String, dynamic> _$MeasurementCategoryToJson(MeasurementCategory instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'unit': instance.unit,

View File

@@ -20,8 +20,7 @@ MeasurementEntry _$MeasurementEntryFromJson(Map<String, dynamic> json) {
);
}
Map<String, dynamic> _$MeasurementEntryToJson(MeasurementEntry instance) =>
<String, dynamic>{
Map<String, dynamic> _$MeasurementEntryToJson(MeasurementEntry instance) => <String, dynamic>{
'id': instance.id,
'category': instance.category,
'date': toDate(instance.date),

View File

@@ -40,8 +40,7 @@ Ingredient _$IngredientFromJson(Map<String, dynamic> json) {
);
}
Map<String, dynamic> _$IngredientToJson(Ingredient instance) =>
<String, dynamic>{
Map<String, dynamic> _$IngredientToJson(Ingredient instance) => <String, dynamic>{
'id': instance.id,
'code': instance.code,
'name': instance.name,

View File

@@ -13,16 +13,14 @@ IngredientWeightUnit _$IngredientWeightUnitFromJson(Map<String, dynamic> json) {
);
return IngredientWeightUnit(
id: json['id'] as int,
weightUnit:
WeightUnit.fromJson(json['weight_unit'] as Map<String, dynamic>),
weightUnit: WeightUnit.fromJson(json['weight_unit'] as Map<String, dynamic>),
ingredient: Ingredient.fromJson(json['ingredient'] as Map<String, dynamic>),
grams: json['grams'] as int,
amount: (json['amount'] as num).toDouble(),
);
}
Map<String, dynamic> _$IngredientWeightUnitToJson(
IngredientWeightUnit instance) =>
Map<String, dynamic> _$IngredientWeightUnitToJson(IngredientWeightUnit instance) =>
<String, dynamic>{
'id': instance.id,
'weight_unit': instance.weightUnit,

View File

@@ -9,14 +9,7 @@ part of 'log.dart';
Log _$LogFromJson(Map<String, dynamic> json) {
$checkKeys(
json,
requiredKeys: const [
'id',
'plan',
'datetime',
'ingredient',
'weight_unit',
'amount'
],
requiredKeys: const ['id', 'plan', 'datetime', 'ingredient', 'weight_unit', 'amount'],
);
return Log(
id: json['id'] as int?,

View File

@@ -18,8 +18,7 @@ NutritionalPlan _$NutritionalPlanFromJson(Map<String, dynamic> json) {
);
}
Map<String, dynamic> _$NutritionalPlanToJson(NutritionalPlan instance) =>
<String, dynamic>{
Map<String, dynamic> _$NutritionalPlanToJson(NutritionalPlan instance) => <String, dynamic>{
'id': instance.id,
'description': instance.description,
'creation_date': toDate(instance.creationDate),

View File

@@ -17,8 +17,7 @@ WeightUnit _$WeightUnitFromJson(Map<String, dynamic> json) {
);
}
Map<String, dynamic> _$WeightUnitToJson(WeightUnit instance) =>
<String, dynamic>{
Map<String, dynamic> _$WeightUnitToJson(WeightUnit instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
};

View File

@@ -17,8 +17,7 @@ RepetitionUnit _$RepetitionUnitFromJson(Map<String, dynamic> json) {
);
}
Map<String, dynamic> _$RepetitionUnitToJson(RepetitionUnit instance) =>
<String, dynamic>{
Map<String, dynamic> _$RepetitionUnitToJson(RepetitionUnit instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
};

View File

@@ -9,14 +9,7 @@ part of 'session.dart';
WorkoutSession _$WorkoutSessionFromJson(Map<String, dynamic> json) {
$checkKeys(
json,
requiredKeys: const [
'id',
'workout',
'date',
'impression',
'time_start',
'time_end'
],
requiredKeys: const ['id', 'workout', 'date', 'impression', 'time_start', 'time_end'],
);
return WorkoutSession()
..id = json['id'] as int?
@@ -28,8 +21,7 @@ WorkoutSession _$WorkoutSessionFromJson(Map<String, dynamic> json) {
..timeEnd = stringToTime(json['time_end'] as String?);
}
Map<String, dynamic> _$WorkoutSessionToJson(WorkoutSession instance) =>
<String, dynamic>{
Map<String, dynamic> _$WorkoutSessionToJson(WorkoutSession instance) => <String, dynamic>{
'id': instance.id,
'workout': instance.workoutId,
'date': toDate(instance.date),

View File

@@ -17,8 +17,7 @@ WeightUnit _$WeightUnitFromJson(Map<String, dynamic> json) {
);
}
Map<String, dynamic> _$WeightUnitToJson(WeightUnit instance) =>
<String, dynamic>{
Map<String, dynamic> _$WeightUnitToJson(WeightUnit instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
};

View File

@@ -19,8 +19,7 @@ WorkoutPlan _$WorkoutPlanFromJson(Map<String, dynamic> json) {
);
}
Map<String, dynamic> _$WorkoutPlanToJson(WorkoutPlan instance) =>
<String, dynamic>{
Map<String, dynamic> _$WorkoutPlanToJson(WorkoutPlan instance) => <String, dynamic>{
'id': instance.id,
'creation_date': instance.creationDate.toIso8601String(),
'name': instance.name,

View File

@@ -242,66 +242,68 @@ class _DashboardWeightWidgetState extends State<DashboardWeightWidget> {
Widget build(BuildContext context) {
weightEntriesData = Provider.of<BodyWeightProvider>(context, listen: false);
return Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(
AppLocalizations.of(context).weight,
style: Theme.of(context).textTheme.headline4,
return Consumer<BodyWeightProvider>(
builder: (context, workoutProvider, child) => Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(
AppLocalizations.of(context).weight,
style: Theme.of(context).textTheme.headline4,
),
leading: const FaIcon(
FontAwesomeIcons.weight,
color: Colors.black,
),
trailing: IconButton(
icon: const Icon(Icons.add),
onPressed: () async {
Navigator.pushNamed(
context,
FormScreen.routeName,
arguments: FormScreenArguments(
AppLocalizations.of(context).newEntry,
WeightForm(),
),
);
},
),
),
leading: const FaIcon(
FontAwesomeIcons.weight,
color: Colors.black,
),
trailing: IconButton(
icon: const Icon(Icons.add),
onPressed: () async {
Navigator.pushNamed(
context,
FormScreen.routeName,
arguments: FormScreenArguments(
Column(
children: [
if (weightEntriesData.items.isNotEmpty)
Column(
children: [
Container(
padding: const EdgeInsets.all(15),
height: 180,
child: MeasurementChartWidget(weightEntriesData.items
.map((e) => MeasurementChartEntry(e.weight, e.date))
.toList()),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
TextButton(
child: Text(AppLocalizations.of(context).goToDetailPage),
onPressed: () {
Navigator.of(context).pushNamed(WeightScreen.routeName);
}),
],
),
],
)
else
NothingFound(
AppLocalizations.of(context).noWeightEntries,
AppLocalizations.of(context).newEntry,
WeightForm(),
),
);
},
],
),
),
Column(
children: [
if (weightEntriesData.items.isNotEmpty)
Column(
children: [
Container(
padding: const EdgeInsets.all(15),
height: 180,
child: MeasurementChartWidget(weightEntriesData.items
.map((e) => MeasurementChartEntry(e.weight, e.date))
.toList()),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
TextButton(
child: Text(AppLocalizations.of(context).goToDetailPage),
onPressed: () {
Navigator.of(context).pushNamed(WeightScreen.routeName);
}),
],
),
],
)
else
NothingFound(
AppLocalizations.of(context).noWeightEntries,
AppLocalizations.of(context).newEntry,
WeightForm(),
),
],
),
],
],
),
),
);
}

View File

@@ -23,16 +23,16 @@ import 'package:wger/providers/nutrition.dart';
import 'package:wger/screens/nutritional_plan_screen.dart';
class NutritionalPlansList extends StatelessWidget {
final NutritionPlansProvider _nutritrionProvider;
const NutritionalPlansList(this._nutritrionProvider);
final NutritionPlansProvider _nutritionProvider;
const NutritionalPlansList(this._nutritionProvider);
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(10.0),
itemCount: _nutritrionProvider.items.length,
itemCount: _nutritionProvider.items.length,
itemBuilder: (context, index) {
final currentPlan = _nutritrionProvider.items[index];
final currentPlan = _nutritionProvider.items[index];
return Dismissible(
key: Key(currentPlan.id.toString()),
confirmDismiss: (direction) async {
@@ -56,7 +56,7 @@ class NutritionalPlansList extends StatelessWidget {
),
onPressed: () {
// Confirmed, delete the workout
_nutritrionProvider.deletePlan(currentPlan.id!);
_nutritionProvider.deletePlan(currentPlan.id!);
// Close the popup
Navigator.of(contextDialog).pop();

View File

@@ -32,28 +32,28 @@ dependencies:
sdk: flutter
android_metadata: ^0.2.1
camera: ^0.9.4+16
camera: ^0.9.8
charts_flutter: ^0.12.0
collection: ^1.15.0-nullsafety.4
cupertino_icons: ^1.0.0
cupertino_icons: ^1.0.5
equatable: ^2.0.3
flutter_calendar_carousel: ^2.0.3
flutter_calendar_carousel: ^2.2.0
flutter_html: ^2.1.2
flutter_typeahead: ^3.2.5
font_awesome_flutter: ">=9.1.0 <11.0.0"
http: ^0.13.3
image_picker: ^0.8.4+11
image_picker: ^0.8.4+9
intl: ^0.17.0
json_annotation: ^4.5.0
version: ^2.0.0
version: ^3.0.2
package_info: ^2.0.2
provider: ^6.0.1
rive: ^0.8.1
shared_preferences: ^2.0.13
table_calendar: ^3.0.2
url_launcher: ^6.1.0
provider: ^6.0.3
rive: ^0.9.0
shared_preferences: ^2.0.15
table_calendar: ^3.0.6
url_launcher: ^6.1.4
flutter_barcode_scanner: ^2.0.0
video_player: ^2.4.0
video_player: ^2.4.5
multi_select_flutter: ^4.0.0
flutter_svg: ^0.23.0+1
@@ -62,10 +62,10 @@ dev_dependencies:
sdk: flutter
integration_test:
sdk: flutter
build_runner: ^2.1.2
flutter_launcher_icons: ^0.9.1
build_runner: ^2.1.11
flutter_launcher_icons: ^0.9.3
json_serializable: ^6.2.0
mockito: ^5.1.0
mockito: ^5.2.0
network_image_mock: ^2.0.1
flutter_lints: ^1.0.4
cider: ^0.1.1

View File

@@ -21,41 +21,36 @@ import 'package:wger/providers/measurement.dart' as _i4;
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
class _FakeWgerBaseProvider_0 extends _i1.Fake implements _i2.WgerBaseProvider {
}
class _FakeWgerBaseProvider_0 extends _i1.Fake implements _i2.WgerBaseProvider {}
class _FakeMeasurementCategory_1 extends _i1.Fake
implements _i3.MeasurementCategory {}
class _FakeMeasurementCategory_1 extends _i1.Fake implements _i3.MeasurementCategory {}
/// A class which mocks [MeasurementProvider].
///
/// See the documentation for Mockito's code generation for more information.
class MockMeasurementProvider extends _i1.Mock
implements _i4.MeasurementProvider {
class MockMeasurementProvider extends _i1.Mock implements _i4.MeasurementProvider {
MockMeasurementProvider() {
_i1.throwOnMissingStub(this);
}
@override
_i2.WgerBaseProvider get baseProvider =>
(super.noSuchMethod(Invocation.getter(#baseProvider),
returnValue: _FakeWgerBaseProvider_0()) as _i2.WgerBaseProvider);
(super.noSuchMethod(Invocation.getter(#baseProvider), returnValue: _FakeWgerBaseProvider_0())
as _i2.WgerBaseProvider);
@override
List<_i3.MeasurementCategory> get categories =>
(super.noSuchMethod(Invocation.getter(#categories),
returnValue: <_i3.MeasurementCategory>[])
(super.noSuchMethod(Invocation.getter(#categories), returnValue: <_i3.MeasurementCategory>[])
as List<_i3.MeasurementCategory>);
@override
bool get hasListeners =>
(super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false)
as bool);
(super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) as bool);
@override
void clear() => super.noSuchMethod(Invocation.method(#clear, []),
returnValueForMissingStub: null);
void clear() =>
super.noSuchMethod(Invocation.method(#clear, []), returnValueForMissingStub: null);
@override
_i3.MeasurementCategory findCategoryById(int? id) => (super.noSuchMethod(
Invocation.method(#findCategoryById, [id]),
returnValue: _FakeMeasurementCategory_1()) as _i3.MeasurementCategory);
_i3.MeasurementCategory findCategoryById(int? id) =>
(super.noSuchMethod(Invocation.method(#findCategoryById, [id]),
returnValue: _FakeMeasurementCategory_1()) as _i3.MeasurementCategory);
@override
_i5.Future<void> fetchAndSetCategories() =>
(super.noSuchMethod(Invocation.method(#fetchAndSetCategories, []),
@@ -67,10 +62,10 @@ class MockMeasurementProvider extends _i1.Mock
returnValue: Future<void>.value(),
returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
@override
_i5.Future<void> fetchAndSetAllCategoriesAndEntries() => (super.noSuchMethod(
Invocation.method(#fetchAndSetAllCategoriesAndEntries, []),
returnValue: Future<void>.value(),
returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
_i5.Future<void> fetchAndSetAllCategoriesAndEntries() =>
(super.noSuchMethod(Invocation.method(#fetchAndSetAllCategoriesAndEntries, []),
returnValue: Future<void>.value(),
returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
@override
_i5.Future<void> addCategory(_i3.MeasurementCategory? category) =>
(super.noSuchMethod(Invocation.method(#addCategory, [category]),
@@ -83,8 +78,7 @@ class MockMeasurementProvider extends _i1.Mock
returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
@override
_i5.Future<void> editCategory(int? id, String? newName, String? newUnit) =>
(super.noSuchMethod(
Invocation.method(#editCategory, [id, newName, newUnit]),
(super.noSuchMethod(Invocation.method(#editCategory, [id, newName, newUnit]),
returnValue: Future<void>.value(),
returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
@override
@@ -98,26 +92,23 @@ class MockMeasurementProvider extends _i1.Mock
returnValue: Future<void>.value(),
returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
@override
_i5.Future<void> editEntry(int? id, int? categoryId, num? newValue,
String? newNotes, DateTime? newDate) =>
_i5.Future<void> editEntry(
int? id, int? categoryId, num? newValue, String? newNotes, DateTime? newDate) =>
(super.noSuchMethod(
Invocation.method(
#editEntry, [id, categoryId, newValue, newNotes, newDate]),
Invocation.method(#editEntry, [id, categoryId, newValue, newNotes, newDate]),
returnValue: Future<void>.value(),
returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
@override
void addListener(_i7.VoidCallback? listener) =>
super.noSuchMethod(Invocation.method(#addListener, [listener]),
returnValueForMissingStub: null);
void addListener(_i7.VoidCallback? listener) => super
.noSuchMethod(Invocation.method(#addListener, [listener]), returnValueForMissingStub: null);
@override
void removeListener(_i7.VoidCallback? listener) =>
super.noSuchMethod(Invocation.method(#removeListener, [listener]),
returnValueForMissingStub: null);
@override
void dispose() => super.noSuchMethod(Invocation.method(#dispose, []),
returnValueForMissingStub: null);
void dispose() =>
super.noSuchMethod(Invocation.method(#dispose, []), returnValueForMissingStub: null);
@override
void notifyListeners() =>
super.noSuchMethod(Invocation.method(#notifyListeners, []),
returnValueForMissingStub: null);
super.noSuchMethod(Invocation.method(#notifyListeners, []), returnValueForMissingStub: null);
}

View File

@@ -24,8 +24,7 @@ import 'package:mockito/mockito.dart' as _i1;
class _FakeResponse_0 extends _i1.Fake implements _i2.Response {}
class _FakeStreamedResponse_1 extends _i1.Fake implements _i3.StreamedResponse {
}
class _FakeStreamedResponse_1 extends _i1.Fake implements _i3.StreamedResponse {}
/// A class which mocks [Client].
///
@@ -38,71 +37,49 @@ class MockClient extends _i1.Mock implements _i4.Client {
@override
_i5.Future<_i2.Response> head(Uri? url, {Map<String, String>? headers}) =>
(super.noSuchMethod(Invocation.method(#head, [url], {#headers: headers}),
returnValue: Future<_i2.Response>.value(_FakeResponse_0()))
as _i5.Future<_i2.Response>);
returnValue: Future<_i2.Response>.value(_FakeResponse_0())) as _i5.Future<_i2.Response>);
@override
_i5.Future<_i2.Response> get(Uri? url, {Map<String, String>? headers}) =>
(super.noSuchMethod(Invocation.method(#get, [url], {#headers: headers}),
returnValue: Future<_i2.Response>.value(_FakeResponse_0()))
as _i5.Future<_i2.Response>);
returnValue: Future<_i2.Response>.value(_FakeResponse_0())) as _i5.Future<_i2.Response>);
@override
_i5.Future<_i2.Response> post(Uri? url,
{Map<String, String>? headers,
Object? body,
_i6.Encoding? encoding}) =>
{Map<String, String>? headers, Object? body, _i6.Encoding? encoding}) =>
(super.noSuchMethod(
Invocation.method(#post, [url],
{#headers: headers, #body: body, #encoding: encoding}),
returnValue: Future<_i2.Response>.value(_FakeResponse_0()))
as _i5.Future<_i2.Response>);
Invocation.method(#post, [url], {#headers: headers, #body: body, #encoding: encoding}),
returnValue: Future<_i2.Response>.value(_FakeResponse_0())) as _i5.Future<_i2.Response>);
@override
_i5.Future<_i2.Response> put(Uri? url,
{Map<String, String>? headers,
Object? body,
_i6.Encoding? encoding}) =>
{Map<String, String>? headers, Object? body, _i6.Encoding? encoding}) =>
(super.noSuchMethod(
Invocation.method(#put, [url],
{#headers: headers, #body: body, #encoding: encoding}),
returnValue: Future<_i2.Response>.value(_FakeResponse_0()))
as _i5.Future<_i2.Response>);
Invocation.method(#put, [url], {#headers: headers, #body: body, #encoding: encoding}),
returnValue: Future<_i2.Response>.value(_FakeResponse_0())) as _i5.Future<_i2.Response>);
@override
_i5.Future<_i2.Response> patch(Uri? url,
{Map<String, String>? headers,
Object? body,
_i6.Encoding? encoding}) =>
{Map<String, String>? headers, Object? body, _i6.Encoding? encoding}) =>
(super.noSuchMethod(
Invocation.method(#patch, [url],
{#headers: headers, #body: body, #encoding: encoding}),
returnValue: Future<_i2.Response>.value(_FakeResponse_0()))
as _i5.Future<_i2.Response>);
Invocation.method(#patch, [url], {#headers: headers, #body: body, #encoding: encoding}),
returnValue: Future<_i2.Response>.value(_FakeResponse_0())) as _i5.Future<_i2.Response>);
@override
_i5.Future<_i2.Response> delete(Uri? url,
{Map<String, String>? headers,
Object? body,
_i6.Encoding? encoding}) =>
{Map<String, String>? headers, Object? body, _i6.Encoding? encoding}) =>
(super.noSuchMethod(
Invocation.method(#delete, [url],
{#headers: headers, #body: body, #encoding: encoding}),
returnValue: Future<_i2.Response>.value(_FakeResponse_0()))
as _i5.Future<_i2.Response>);
Invocation.method(#delete, [url], {#headers: headers, #body: body, #encoding: encoding}),
returnValue: Future<_i2.Response>.value(_FakeResponse_0())) as _i5.Future<_i2.Response>);
@override
_i5.Future<String> read(Uri? url, {Map<String, String>? headers}) =>
(super.noSuchMethod(Invocation.method(#read, [url], {#headers: headers}),
returnValue: Future<String>.value('')) as _i5.Future<String>);
@override
_i5.Future<_i7.Uint8List> readBytes(Uri? url,
{Map<String, String>? headers}) =>
(super.noSuchMethod(
Invocation.method(#readBytes, [url], {#headers: headers}),
returnValue: Future<_i7.Uint8List>.value(_i7.Uint8List(0)))
as _i5.Future<_i7.Uint8List>);
_i5.Future<_i7.Uint8List> readBytes(Uri? url, {Map<String, String>? headers}) =>
(super.noSuchMethod(Invocation.method(#readBytes, [url], {#headers: headers}),
returnValue: Future<_i7.Uint8List>.value(_i7.Uint8List(0))) as _i5.Future<_i7.Uint8List>);
@override
_i5.Future<_i3.StreamedResponse> send(_i8.BaseRequest? request) =>
(super.noSuchMethod(Invocation.method(#send, [request]),
returnValue:
Future<_i3.StreamedResponse>.value(_FakeStreamedResponse_1()))
returnValue: Future<_i3.StreamedResponse>.value(_FakeStreamedResponse_1()))
as _i5.Future<_i3.StreamedResponse>);
@override
void close() => super.noSuchMethod(Invocation.method(#close, []),
returnValueForMissingStub: null);
void close() =>
super.noSuchMethod(Invocation.method(#close, []), returnValueForMissingStub: null);
}