Merge branch 'master' into adyhnat_master

# Conflicts:
#	.gitignore
#	pubspec.lock
This commit is contained in:
Roland Geider
2022-01-23 20:59:37 +01:00
56 changed files with 1914 additions and 164 deletions

View File

@@ -29,7 +29,7 @@ jobs:
- name: Decrypt config files - name: Decrypt config files
run: | run: |
cd ./fastlane/android/envfiles cd ./fastlane/metadata/envfiles
chmod +x ./decrypt_secrets.sh chmod +x ./decrypt_secrets.sh
./decrypt_secrets.sh ./decrypt_secrets.sh
env: env:
@@ -81,7 +81,6 @@ jobs:
uses: maierj/fastlane-action@v2.1.0 uses: maierj/fastlane-action@v2.1.0
with: with:
lane: production lane: production
subdirectory: android
- name: Make Github release - name: Make Github release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1

9
.gitignore vendored
View File

@@ -43,8 +43,7 @@ app.*.map.json
# Remember to never publicly share your keystore. # Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
/fastlane/metadata/android/envfiles/playstore.json /fastlane/metadata/envfiles/playstore.json
/fastlane/metadata/android/envfiles/wger.properties /fastlane/metadata/envfiles/wger.properties
/fastlane/metadata/android/envfiles/keys.jks /fastlane/metadata/envfiles/keys.jks
/fastlane/metadata/android/envfiles/key.properties /fastlane/metadata/envfiles/key.properties
/coverage/lcov.info

View File

@@ -1,25 +1,11 @@
🚀 Features: 🚀 Features:
* Allow users to give meals a description #89 * ...
* Allow users to delete workout log entries #97
* Allow users to log individual ingredients/products #114
* New loading animation during first run #99
* Added reference from log to meal #105
🐛 Bug Fixes: 🐛 Bug Fixes:
* Improve usability for workout logs #91 * ...
* Preselect correct time in session form #93
* Fixed infinite loader bug during auth #96
* Fix error not showing up in auth screen #102
* Fix for chart legend bug #112
* Fix for RiR slider #113
🧰 Maintenance: 🧰 Maintenance:
* Better linting #87 #98 * ...
* Improve automatic build system for publication on play store
* Notify the user when saving entries in gym mode #92
* Consistenly display nutritional values #94
* Add minimum required server version #29
* Make order field of sets required #109

View File

@@ -1,10 +1,10 @@
🚀 Features: 🚀 Features:
* * ...
🐛 Bug Fixes: 🐛 Bug Fixes:
* * ...
🧰 Maintenance: 🧰 Maintenance:
* * ...

View File

@@ -7,19 +7,21 @@ 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! If you want to contribute, hop on the Discord server and say hi!
<p float="left"> <p>
<img src="https://github.com/wger-project/flutter/blob/master/android/fastlane/metadata/android/en-US/images/phoneScreenshots/01%20-%20workout%20plan.png?raw=true" width="200" /> <img src="https://raw.githubusercontent.com/wger-project/flutter/master/fastlane/metadata/android/en-US/images/phoneScreenshots/01%20-%20workout%20plan.png" width="200" />
<img src="https://github.com/wger-project/flutter/blob/master/android/fastlane/metadata/android/en-US/images/phoneScreenshots/02%20-%20workout%20log.png?raw=true" width="200" /> <img src="https://raw.githubusercontent.com/wger-project/flutter/master/fastlane/metadata/android/en-US/images/phoneScreenshots/02%20-%20workout%20log.png" width="200" />
<img src="https://github.com/wger-project/flutter/blob/master/android/fastlane/metadata/android/en-US/images/phoneScreenshots/04%20-%20nutritional%20plan.png?raw=true" width="200" /> <img src="https://raw.githubusercontent.com/wger-project/flutter/master/fastlane/metadata/android/en-US/images/phoneScreenshots/04%20-%20nutritional%20plan.png" width="200" />
</p> </p>
## Installation ## Installation
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" [<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
alt="Get it on Google Play" alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=de.wger.flutter) height="80">](https://play.google.com/store/apps/details?id=de.wger.flutter)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/de.wger.flutter/)
## Development ## Development
@@ -50,6 +52,9 @@ on your local instance and then run ``python3 manage.py add-user-rest theusernam
You can later list all the registered users with: ``python3 manage.py list-users-api`` You can later list all the registered users with: ``python3 manage.py list-users-api``
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/de.wger.flutter)
### 4 ### 4
Start the application with ``flutter run`` or use your IDE Start the application with ``flutter run`` or use your IDE

View File

@@ -27,14 +27,14 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
// Keys for the android play store // Keys for the android play store
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('../fastlane/metadata/android/envfiles/key.properties') def keystorePropertiesFile = rootProject.file('../fastlane/metadata/envfiles/key.properties')
if (keystorePropertiesFile.exists()) { if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
} }
// Key for wger.de REST API // Key for wger.de REST API
def wgerProperties = new Properties() def wgerProperties = new Properties()
def localMapsPropertiesFile = rootProject.file('../fastlane/metadata/android/envfiles/wger.properties') def localMapsPropertiesFile = rootProject.file('../fastlane/metadata/envfiles/wger.properties')
if (localMapsPropertiesFile.exists()) { if (localMapsPropertiesFile.exists()) {
project.logger.info('Load maps properties from local file') project.logger.info('Load maps properties from local file')
localMapsPropertiesFile.withReader('UTF-8') { reader -> localMapsPropertiesFile.withReader('UTF-8') { reader ->

View File

@@ -30,6 +30,7 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -29,6 +29,7 @@ platform :android do
lane :test_configuration do lane :test_configuration do
begin begin
upload_to_play_store( upload_to_play_store(
json_key: 'fastlane/metadata/envfiles/playstore.json',
track: 'production', track: 'production',
validate_only: true, validate_only: true,
aab: './build/app/outputs/bundle/release/app-release.aab', aab: './build/app/outputs/bundle/release/app-release.aab',
@@ -41,6 +42,7 @@ platform :android do
lane :production do lane :production do
begin begin
upload_to_play_store( upload_to_play_store(
json_key: 'fastlane/metadata/envfiles/playstore.json',
track: 'production', track: 'production',
aab: './build/app/outputs/bundle/release/app-release.aab', aab: './build/app/outputs/bundle/release/app-release.aab',
skip_upload_metadata: false, skip_upload_metadata: false,
@@ -56,6 +58,7 @@ platform :android do
lane :update_alpha do lane :update_alpha do
begin begin
upload_to_play_store( upload_to_play_store(
json_key: 'fastlane/metadata/envfiles/playstore.json',
track: 'alpha', track: 'alpha',
aab: './build/app/outputs/bundle/release/app-release.aab', aab: './build/app/outputs/bundle/release/app-release.aab',
skip_upload_metadata: true, skip_upload_metadata: true,

View File

@@ -1,47 +1,59 @@
fastlane documentation fastlane documentation
================ ----
# Installation # Installation
Make sure you have the latest version of the Xcode command line tools installed: Make sure you have the latest version of the Xcode command line tools installed:
``` ```sh
xcode-select --install xcode-select --install
``` ```
Install _fastlane_ using For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
```
[sudo] gem install fastlane -NV
```
or alternatively using `brew install fastlane`
# Available Actions # Available Actions
### playstore ### playstore
```sh
[bundle exec] fastlane playstore
``` ```
fastlane playstore
```
---- ----
## Android ## Android
### android test_configuration ### android test_configuration
```sh
[bundle exec] fastlane android test_configuration
``` ```
fastlane android test_configuration
``` Check playstore configuration
Check configuration
### android production ### android production
```sh
[bundle exec] fastlane android production
``` ```
fastlane android production
```
Upload app to production Upload app to production
### android update_alpha ### android update_alpha
```sh
[bundle exec] fastlane android update_alpha
``` ```
fastlane android update_alpha
```
Upload closed alpha app and update store entry Upload closed alpha app and update store entry
---- ----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).

View File

@@ -0,0 +1 @@
Registre de la forma física, entrenament, nutrició i pes corporal

View File

@@ -1,39 +1,39 @@
From fitness lovers to fitness lovers get your health organized with WGER, your Workout Manager! Por amantes del fitness para amantes del fitness ¡Organiza tu salud con WGER, tu gestor de entrenamientos!
Have you already found your #1 fitness app and do you love to create your own sports routines? No matter what type of sporty beast you are we all have something in common: We love to keep track of our health data <3 ¿Has encontrado ya tu app de fitnes nº1 y te encanta crear tus propias rutinas deportivas? No importa qué clase de bestia deportista eres todos tenemos algo en común: amamos hacer el seguimiento de los datos de nuestra salud <3
So we dont judge you for still managing your fitness journey with your handy little workout log book but welcome to 2021! Así que no vamos a juzgarte si aún gestionas tu andadura en el fitness con tu pequeño y práctico diario de entrenamientos, ¡pero te damos la bienvenida a 2022!
We have developed a 100% free digital health and fitness tracker app for you, sized down to the most relevant features to make your life easier. Get started, keep training and celebrate your progress! Hemos desarrollado una app de seguimiento digital de la salud y el fitness 100% libre, reducida a las funcionalidades más relevantes para hacerte la vida más fácil. ¡Empieza ya, no pares tomar datos y celebra tus progresos!
wger is an Open Source project and all about: wger es un proyecto de código abierto centrado en:
* Your Body * Tu Cuerpo
* Your Workouts * Tus Entrenamientos
* Your Progress * Tus Progresos
* Your Data * Tus Datos
Your Body: Tu Cuerpo:
No need to google for the ingredients of your favourite treats choose your daily meals from more than 78000 products and see the nutritional values. Add meals to the nutritional plan and keep an overview of your diet in the calendar. No hace falta buscar en Google los ingredientes de tus caprichos favoritos elige tus comidas diarias entre más de 78000 productos para ver sus valores nutricionales. Añade comidas a tu plan nutricional y ten una vista general de tu dieta en el calendario.
Your Workouts: Tus Entrenamientos:
You know what is best for your body. Create your own workouts out of a growing variety from 200 different exercises. Then, use the Gym Mode to guide you through the training while you log your weights with one tap. Sabes qué es lo mejor para tu cuerpo. Crea tus propios entrenamientos entre una gama creciente de 200 ejercicios diferentes. Después, usa el Modo Gimnasio para que te guíe a través del entrenamiento mientras registras tu peso con un toque de dedo.
Your Progress: Tus Progresos:
Never lose sight of your goals. Track your weight and keep your statistics. Nunca pierdas de vista tus objetivos. Lleva la cuenta de tu peso y mantén tus estasticas.
Your Data: Tus Datos:
wger is your personalized fitness diary but you own your data. Use the REST API to access and do amazing things with it. wger es tu diario de fitness personalizado pero tus datos te pertenecen a ti. Usa la API REST para acceder y hacer cosas increíbles con ella.
Please note: This free app is not based on additional fundings and we dont ask you to donate money. More than that it is a community project which is growing constantly. So be prepared for new features anytime! Por favor, ten en cuenta: esta app no se basa en financiación adicional y no te pedimos que dones dinero. Más que eso, es un proyecto comunitario que no para de crecer. ¡Así que estate preparado para nuevas funcionalidades en cualquier momento!
#OpenSource what does that mean? #OpenSource ¿qué significa?
Open Source means that the whole source code for this app and the server it talks to is free and available to anybody: Open Source (o código abierto) significa que la totalidad del código fuente para esta app y el servidor con el que habla es libre y está disponible para cualquiera:
* Do you want to run wger on your own server for you or your local gym? Go ahead! * ¿Quieres usar wger en tu propio servidor para tu propio ginmasio local? ¡Adelante!
* Do you miss a feature and want to implement it? Start now! * ¿Echas de menos una funcionalidad y quieres implementarla? ¡Empieza ya!
* Do you want to check that nothing is being sent anywhere? You can! * ¿Quieres asegurarte de que nada se envía a ningún sitio? ¡Puedes!
Join our community and become a part of sport enthusiasts and IT geeks from all over the world. We keep working on adjusting and optimizing the app customized to our needs. We love your input so feel free to jump in anytime and contribute your wishes and ideas! Entra en nuestra comunidad y únete a entusiastas de los deportes y genios de la informática de todo el mundo. No paramos de trabajar en afinar y optimizar la app adaptada a nuestras necesidades. Nos encantará tu aporte, así que no dudes en meterte en cualquier momento y contribuir con tus deseos e ideas!
-> find the source code on https://github.com/wger-project -> encuentra el código fuente en https://github.com/wger-project
-> ask your questions or just say hello on our discord Server https://discord.gg/rPWFv6W -> haz tus preguntas o simplemente di hola en nuestro servidor de Discord https://discord.gg/rPWFv6W

View File

@@ -1 +1 @@
wger Workout Manager wger Workout Manager

View File

@@ -0,0 +1,39 @@
अपना स्वास्थ का ख्याल रखिये WGER, आपका वर्कआउट मैनेजर के साथ!
क्या आपने अपना #1 फिटनेस आप चुना है और क्या आप अपना खुद स्पोर्ट रूटीन बनाते है? चाहे आपका स्वास्थ किसी भी प्रकार का हो: हम सब अपना हेल्थ डाटा को ट्रैक करना चाहते है <3
हम आपको अपने फिटनेस जर्नी को एक छोटे लॉग पे दर्ज करने के लिए जज नहीं करते। इक्कीसवीं सदी में आपका स्वागत है!
हमने आपले लिए के 100% मुफ्त डिजिटल हेल्थ और फिटनेस ट्रैकर बनाया है, सबसे महत्वपूर्ण फीचर्स के साथ जिससे आपका जीवन बोहोत आसान बन जायेगा। तो चलिए, आगे बारे, ट्रैन करे, और अपने प्रोग्रेस का मज़े लीजिये!
wger एक ओपनसोर्स प्रोजेक्ट है और निम्मलिखित चीज़ो से सम्बन्धित है:
* आपका शरीर
* आपका वर्कऑउट्स
* आपका प्रोग्रेस
* आपका डाटा
आपका शरीर:
आपको अपना मनपसंद खानो का अवयव गूगल में ढूंढ़ना नहीं पड़ेगा - अपना दैनिक भोजन 78000 से ज़्यादा प्रोडक्ट्स में से चून्हे और उनका न्यूट्रिशनल वैल्यूज देखे। नुट्रिशन प्लान में अपना भोजन दर्ज कीजिये और अपने कैलेंडर में अपना डाइट को ट्रैक करे।
आपका वर्कऑउट्स:
आपको पता है की आपके बॉडी के लिए सबसे अच्छा क्या है। अपना वर्कऑउट्स खुद बनाये। उसके बाद जिम मोड का इस्तेमाल करिये आपके ट्रेनिंग के लिए।
आपका प्रोग्रेस:
कभी अपना गोल्स के बारे में न भूले। अपना वज़न और स्टेटिस्टिक्स को ट्रैक कीजिये।
आपका डाटा:
wger आपका निजीकृत फ़िटनेस डायरी है - आप अपना डाटा के मालिक है। हमारा REST API का प्रयोग करके कुछ मज़ेदार काम करे।
क्रियण नोट कीजिये: यह मुफ्त आप कोई भी एडिशनल फ़ंडिंग्स पे नहीं चलता और हम आपको पैसा डोनेट करने के लिए नहीं पूछते। यह एक कम्युनिटी प्रोजेक्ट है को हर वक्त बढ़ रहा है। तो नए फीटर्स के लिए हमेशा तैयार रहे!
#ओपनसोर्स - इसका अर्थ क्या है?
ओपनसोर्स का अर्थ यह है की इस अप्प का पूरा सोर्स कोड और जिस सर्वर से यह बात करता है - सबके लिए मुफ्त और उपलब्ध है।
* क्या आप wger को अपने लोकल सर्वर में या फिर अपने स्थानीय जिम के लिए प्रयोग करना चाहते है? आगे बढ़िए!
* क्या आपको किसी फीचर का कमी नज़र आया है और आप उसको खुद इम्प्लीमेंट करना चाहते है? अभी शुरू कीजिये!
* क्या आपको देखना है की कुछ भी कही भी नहीं भेजा जा रहा है? आप कर सकते है!
हमारा कम्युनिटी ज्वाइन करिये और संसार के अन्य स्पोर्ट्स एवं IT गीक्स के साथ जुड़े। हम इस अप्प को अपने खुद के लिए ऑप्टिमिसे और बदल ते रहते है। आपका इनपुट भी चाहिए तो कृपया हमारे कम्युनिटी ज्वाइन करिये और अपना विचार धरा हमें बोले।
-> सोर्स कोड यहाँ पाए : https://github.com/wger-project
-> प्रश्न पूछे या फिर केवल नमस्ते बोले हमारे डिस्कॉर्ड सर्वर पे : https://discord.gg/rPWFv6W

View File

@@ -0,0 +1 @@
फिटनेस/वर्कआउट, नुट्रिशन और वज़न ट्रैकर

View File

@@ -0,0 +1 @@
wger Workout Manager

View File

@@ -0,0 +1,39 @@
От любителей фитнеса для любителей фитнеса - организуйте свое здоровье с WGER, вашим менеджером тренировок!
Вы уже нашли свое фитнес-приложение №1 и любите создавать свои собственные спортивные программы? Независимо от того, какой вы спортивный зверь - у всех нас есть нечто общее: мы любим отслеживать данные о своем здоровье <3
Поэтому мы не осуждаем вас за то, что вы все еще ведете свой фитнес-путь с помощью удобного маленького журнала тренировок, но добро пожаловать в 2021 год!
Мы разработали для вас на 100% свободное цифровое приложение для отслеживания состояния здоровья и фитнес-тренировок, сократив его до самых необходимых функций, чтобы облегчить вам жизнь. Начните, продолжайте тренироваться и отмечайте свой прогресс!
wger - это проект с открытым исходным кодом и все о:
* Вашем теле
* Ваших тренировках
* Вашем прогрессе
* Ваших данных
Вашем теле:
Не нужно "гуглить" ингредиенты любимых лакомств - выбирайте ежедневные блюда из более чем 78000 продуктов и смотрите их пищевую ценность. Добавляйте блюда в план питания и следите за своим рационом в календаре.
Ваших тренировках:
Вы знаете, что лучше всего подходит для вашего тела. Создавайте свои собственные тренировки из растущего разнообразия 200 различных упражнений. Затем используйте режим тренажерного зала, чтобы направлять вас во время тренировки, а также регистрируйте свои веса одним касанием.
Вашем прогрессе:
Никогда не теряйте из виду свои цели. Отслеживайте свой вес и ведите статистику.
Ваших данных:
wger - это ваш персональный фитнес-дневник - но ваши данные принадлежат вам. Используйте REST API для доступа к ним и делайте с ними удивительные вещи.
Обратите внимание: это свободное приложение не основано на дополнительном финансировании, и мы не просим вас жертвовать деньги. Более того, это проект сообщества, который постоянно растет. Так что будьте готовы к появлению новых функций в любое время!
#OpenSource #ОткрытыйИсходныйКод - что это значит?
Открытый исходный код означает, что весь исходный код этого приложения и сервера, с которым оно работает, свободен и доступен любому:
* Вы хотите запустить wger на собственном сервере для себя или своего спортзала? Вперед!
* Вам не хватает какой-то функции и вы хотите ее реализовать? Начните прямо сейчас!
* Хотите проверить, что ничего никуда не отправляется? Вы можете!
Присоединяйтесь к нашему сообществу и станьте частью спортивных энтузиастов и IT-гиков со всего мира. Мы продолжаем работать над корректировкой и оптимизацией приложения в соответствии с нашими потребностями. Нам нравится ваш вклад, поэтому не стесняйтесь вступать в сообщество в любое время и высказывать свои пожелания и идеи!
-> найти исходный код на https://github.com/wger-project
-> задавайте свои вопросы или просто поздоровайтесь на нашем discord-сервере https://discord.gg/rPWFv6W

View File

@@ -0,0 +1 @@
Трекер фитнеса/тренировок, питания и веса

View File

@@ -0,0 +1 @@
wger Workout Manager

View File

@@ -0,0 +1 @@
wger Workout Manager

View File

@@ -0,0 +1 @@
wger Workout Manager

Binary file not shown.

524
lib/l10n/app_ca.arb Normal file
View File

@@ -0,0 +1,524 @@
{
"login": "Entra",
"@login": {
"description": "Text for login button"
},
"logout": "Surt",
"@logout": {
"description": "Text for logout button"
},
"usernameValidChars": "Un nom d'usuari només pot contenir lletres, números i els caràcters @, +, ., -, i _",
"@usernameValidChars": {
"description": "Error message when the user tries to register a username with forbidden characters"
},
"passwordsDontMatch": "Les contrasenyes no coincideixen",
"@passwordsDontMatch": {
"description": "Error message when the user enters two different passwords during registration"
},
"passwordTooShort": "La contrasenya és massa curta",
"@passwordTooShort": {
"description": "Error message when the user a password that is too short"
},
"password": "Contrasenya",
"@password": {},
"confirmPassword": "Confirma la contrasenya",
"@confirmPassword": {},
"invalidEmail": "Introdueix una adreça electrònica vàlida",
"@invalidEmail": {
"description": "Error message when the user enters an invalid email"
},
"email": "Adreça electrònica",
"@email": {},
"username": "Nom d'usuari",
"@username": {},
"customServerUrl": "URL de la instància de wger",
"@customServerUrl": {
"description": "Label in the form where the users can enter their own wger instance"
},
"reset": "Reinicialitza",
"@reset": {
"description": "Button text allowing the user to reset the entered values to the default"
},
"registerInstead": "Registra't",
"@registerInstead": {},
"labelBottomNavWorkout": "Entrenament",
"@labelBottomNavWorkout": {
"description": "Label used in bottom navigation, use a short word"
},
"labelWorkoutLogs": "Registes d'entrenament",
"@labelWorkoutLogs": {
"description": "(Workout) logs"
},
"labelWorkoutPlan": "Pla d'entrenament",
"@labelWorkoutPlan": {
"description": "Title for screen workout plan"
},
"labelDashboard": "Consola",
"@labelDashboard": {
"description": "Title for screen dashboard"
},
"successfullyDeleted": "Esborrat",
"@successfullyDeleted": {
"description": "Message when an item was successfully deleted"
},
"successfullySaved": "Desat",
"@successfullySaved": {
"description": "Message when an item was successfully saved"
},
"exercise": "Exercici",
"@exercise": {
"description": "An exercise for a workout"
},
"searchExercise": "Cerca exercici a afegir",
"@searchExercise": {
"description": "Label on set form. Selected exercises are added to the set"
},
"supersetWith": "en conjunt amb",
"@supersetWith": {
"description": "Text used between exercise cards when adding a new set. Translate as something like 'in a superset with'"
},
"equipment": "Equipament",
"@equipment": {
"description": "Equipment needed to perform an exercise"
},
"muscles": "Músculs",
"@muscles": {
"description": "(main) muscles trained by an exercise"
},
"musclesSecondary": "Músculs secundaris",
"@musclesSecondary": {
"description": "secondary muscles trained by an exercise"
},
"category": "Categoria",
"@category": {
"description": "Category for an exercise, ingredient, etc."
},
"newWorkout": "Nou pla d'entrenament",
"@newWorkout": {
"description": "Header when adding a new workout"
},
"noWorkoutPlans": "No tens cap pla d'entrenament",
"@noWorkoutPlans": {
"description": "Message shown when the user has no workout plans"
},
"repetitions": "Repeticions",
"@repetitions": {
"description": "Repetitions for an exercise set"
},
"reps": "Reps",
"@reps": {
"description": "Shorthand for repetitions, used when space constraints are tighter"
},
"rir": "RER",
"@rir": {
"description": "Shorthand for Repetitions In Reserve"
},
"rirNotUsed": "Valor RER no usat",
"@rirNotUsed": {
"description": "Label used in RiR slider when the RiR value is not used/saved for the current setting or log"
},
"repetitionUnit": "Unitat de repetició",
"@repetitionUnit": {},
"set": "Sèrie",
"@set": {
"description": "A set in a workout plan"
},
"sameRepetitions": "Si fas les mateixes repeticions i pesos per a totes les sèries, pots omplir només una línia. Per exemple, per a 4 sèries simplement introdueix 10 per a les repeticions; això esdevé automàticament «4 x 10».",
"@sameRepetitions": {},
"comment": "Comentaris",
"@comment": {
"description": "Comment, additional information"
},
"impression": "Impressió",
"@impression": {
"description": "General impression (e.g. for a workout session) such as good, bad, etc."
},
"notes": "Notes",
"@notes": {
"description": "Personal notes, e.g. for a workout session"
},
"workoutSession": "Sessió d'entrenament",
"@workoutSession": {
"description": "A (logged) workout session"
},
"newDay": "Nou dia",
"@newDay": {},
"newSet": "Nova sèrie",
"@newSet": {
"description": "Header when adding a new set to a workout day"
},
"selectExercises": "Si vols fer una supersèrie, pots cercar diversos exercicis: s'agruparan junts",
"@selectExercises": {},
"plateCalculator": "Plaques",
"@plateCalculator": {
"description": "Label used for the plate calculator in the gym mode"
},
"plateCalculatorNotDivisible": "No es pot assolir aquest pes amb les plaques disponibles",
"@plateCalculatorNotDivisible": {
"description": "Error message when the current weight is not reachable with plates (e.g. 33.1 kg)"
},
"pause": "Pausa",
"@pause": {
"description": "Noun, not an imperative! Label used for the pause when using the gym mode"
},
"jumpTo": "Vés a",
"@jumpTo": {
"description": "Imperative. Label used in popup allowing the user to jump to a specific exercise while in the gym mode"
},
"todaysWorkout": "El teu entrenament avui",
"@todaysWorkout": {},
"logHelpEntriesUnits": "Tingues en compte que només es fa la gràfica de les entrades amb unitats de pes (kg o lb) i repeticions, altres combinacions com ara temps o fins la fallada s'ignoren aquí.",
"@logHelpEntriesUnits": {},
"description": "Descripció",
"@description": {},
"name": "Nom",
"@name": {
"description": "Name for a workout or nutritional plan"
},
"save": "Desa",
"@save": {},
"addSet": "Afegeix sèrie",
"@addSet": {
"description": "Label for the button that adds a set (to a workout day)"
},
"addMeal": "Afegeix àpat",
"@addMeal": {},
"mealLogged": "Àpat registrat al diari",
"@mealLogged": {},
"addIngredient": "Afegeix ingredient",
"@addIngredient": {},
"logIngredient": "Desa al diari nutricional",
"@logIngredient": {},
"nutritionalPlan": "Pla nutricional",
"@nutritionalPlan": {},
"nutritionalDiary": "Diari nutricional",
"@nutritionalDiary": {},
"anErrorOccurred": "S'ha produït un error!",
"@anErrorOccurred": {},
"weight": "Pes",
"@weight": {
"description": "The weight of a workout log or body weight entry"
},
"measurement": "Mesura",
"@measurement": {},
"measurementEntriesHelpText": "La unitat usada per a mesurar la categoria, com ara «cm» o «%»",
"@measurementEntriesHelpText": {},
"start": "Comença",
"@start": {
"description": "Label on button to start the gym mode (i.e., an imperative)"
},
"time": "Temps",
"@time": {
"description": "The time of a meal or workout"
},
"timeStart": "Hora d'inici",
"@timeStart": {
"description": "The starting time of a workout"
},
"timeEnd": "Hora de finalització",
"@timeEnd": {
"description": "The end time of a workout"
},
"ingredient": "Ingredient",
"@ingredient": {},
"energy": "Energia",
"@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": "Macronutrients",
"@macronutrients": {},
"logged": "Registrat",
"@logged": {
"description": "Header for the column of 'logged' nutritional values, i.e. what was eaten"
},
"percentEnergy": "Percentatge d'energia",
"@percentEnergy": {},
"gPerBodyKg": "g per kg de pes corporal",
"@gPerBodyKg": {
"description": "Label used for total sums of e.g. calories or similar in grams per Kg of body weight"
},
"total": "Total",
"@total": {
"description": "Label used for total sums of e.g. calories or similar"
},
"kJ": "kJ",
"@kJ": {
"description": "Energy in a meal in kilo joules, kJ"
},
"g": "g",
"@g": {
"description": "Abbreviation for gram"
},
"protein": "Proteïna",
"@protein": {},
"carbohydrates": "Carbohidrats",
"@carbohydrates": {},
"carbohydratesShort": "C",
"@carbohydratesShort": {
"description": "The first letter or short name of the word 'Carbohydrates', used in overviews"
},
"sugars": "Sucres",
"@sugars": {},
"fat": "Greix",
"@fat": {},
"amount": "Quantitat",
"@amount": {
"description": "The amount (e.g. in grams) of an ingredient in a meal"
},
"unit": "Unitat",
"@unit": {
"description": "The unit used for a repetition (kg, time, etc.)"
},
"newEntry": "Nova entrada",
"@newEntry": {
"description": "Title when adding a new entry such as a weight or log entry"
},
"edit": "Edita",
"@edit": {},
"loadingText": "Carregant...",
"@loadingText": {
"description": "Text to show when entries are being loaded in the background: Loading..."
},
"newNutritionalPlan": "Nou pla nutricional",
"@newNutritionalPlan": {},
"toggleDetails": "Commuta detalls",
"@toggleDetails": {
"description": "Switch to toggle detail / overview"
},
"goToDetailPage": "Vés a la pàgina dels detalls",
"@goToDetailPage": {},
"aboutSourceTitle": "Codi font",
"@aboutSourceTitle": {
"description": "Title for source code section in the about dialog"
},
"aboutBugsTitle": "Teniu cap problema o idea?",
"@aboutBugsTitle": {
"description": "Title for bugs section in the about dialog"
},
"aboutBugsText": "Poseu-vos en contacte si alguna cosa no funciona com s'esperava o si hi ha alguna funció que penseu que manca.",
"@aboutBugsText": {
"description": "Text for bugs section in the about dialog"
},
"aboutContactUsTitle": "Saludeu!",
"@aboutContactUsTitle": {
"description": "Title for contact us section in the about dialog"
},
"aboutContactUsText": "Si voleu xatejar amb nosaltres, entreu al servidor de Discord i poseu-vos en contacte",
"@aboutContactUsText": {
"description": "Text for contact us section in the about dialog"
},
"aboutTranslationTitle": "Traducció",
"@aboutTranslationTitle": {
"description": "Title for translation section in the about dialog"
},
"aboutTranslationText": "Aquesta aplicació es tradueix a weblate. Si voleu ajudar-hi també, feu clic al vincle i comenceu a traduir",
"@aboutTranslationText": {
"description": "Text for translation section in the about dialog"
},
"calendar": "Calendari",
"@calendar": {},
"enterValue": "Introduïu un valor",
"@enterValue": {
"description": "Error message when the user hasn't entered a value on a required field"
},
"nrOfSets": "Sèries per exercici: {nrOfSets}",
"@nrOfSets": {
"description": "Label shown on the slider where the user selects the nr of sets",
"type": "text",
"placeholders": {
"nrOfSets": {}
}
},
"setUnitsAndRir": "Unitats de la sèrie i RER",
"@setUnitsAndRir": {
"description": "Label shown on the slider where the user can toggle showing units and RiR",
"type": "text"
},
"enterValidNumber": "Introduïu un número vàlid",
"@enterValidNumber": {
"description": "Error message when the user has submitted an invalid number (e.g. '3,.,.,.')"
},
"selectIngredient": "Seleccioneu un ingredient",
"@selectIngredient": {
"description": "Error message when the user hasn't selected an ingredient from the autocompleter"
},
"selectImage": "Seleccioneu una imatge",
"@selectImage": {
"description": "Label and error message when the user hasn't selected an image to save"
},
"optionsLabel": "Opcions",
"@optionsLabel": {
"description": "Label for the popup with general app options"
},
"takePicture": "Fes una foto",
"@takePicture": {},
"chooseFromLibrary": "Escull del rodet de fotos",
"@chooseFromLibrary": {},
"appUpdateTitle": "Cal actualitzar",
"@appUpdateTitle": {},
"invalidUrl": "Entra un URL vàlid",
"@invalidUrl": {
"description": "Error message when the user enters an invalid URL, e.g. in the login form"
},
"invalidUsername": "Introdueix un nom d'usuari vàlid",
"@invalidUsername": {
"description": "Error message when the user enters an invalid username"
},
"register": "Registra't",
"@register": {
"description": "Text for registration button"
},
"useDefaultServer": "Usa el servidor per defecte",
"@useDefaultServer": {
"description": "Toggle button allowing users to switch between the default and a custom wger server"
},
"useCustomServer": "Usa un servidor personalitzat",
"@useCustomServer": {
"description": "Toggle button allowing users to switch between the default and a custom wger server"
},
"customServerHint": "Introdueix l'adreça del teu servidor propi, altrament s'usarà el servidor per defecte",
"@customServerHint": {
"description": "Hint text for the form where the users can enter their own wger instance"
},
"loginInstead": "Entra",
"@loginInstead": {},
"labelWorkoutPlans": "Plans d'entrenament",
"@labelWorkoutPlans": {
"description": "Title for screen workout plans"
},
"labelBottomNavNutrition": "Nutrició",
"@labelBottomNavNutrition": {
"description": "Label used in bottom navigation, use a short word"
},
"weightUnit": "Unitat de pes",
"@weightUnit": {},
"setNr": "Sèrie {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": {}
}
},
"dayDescriptionHelp": "Descripció de què es fa aquest dia (p. e. «dia de tracció») o quines parts del cos d'exerciten (p. e. «pit i espatlles»)",
"@dayDescriptionHelp": {},
"aboutDescription": "Gràcies per usar wger! wger és un projecte col·laboratiu de codi obert, fet per entusiastes del fitness d'arreu del món.",
"@aboutDescription": {
"description": "Text in the about dialog"
},
"gymMode": "Mode gimnàs",
"@gymMode": {
"description": "Label when starting the gym mode"
},
"logHelpEntries": "Si en un únic dia hi ha més d'una entrada amb el mateix nombre de repeticions però diferents pesos, al diagrama es mostra només l'entrada amb el pes més gran.",
"@logHelpEntries": {},
"noNutritionalPlans": "No tens plans nutricionals",
"@noNutritionalPlans": {
"description": "Message shown when the user has no nutritional plans"
},
"planned": "Previst",
"@planned": {
"description": "Header for the column of 'planned' nutritional values, i.e. what should be eaten"
},
"timeStartAhead": "L'hora d'inici no pot ser posterior a la de finalització",
"@timeStartAhead": {},
"logMeal": "Registra aquest àpat",
"@logMeal": {},
"searchIngredient": "Cerca ingredient",
"@searchIngredient": {
"description": "Label on ingredient search form"
},
"nutritionalPlans": "Plans nutricionals",
"@nutritionalPlans": {},
"measurements": "Mesures",
"@measurements": {
"description": "Categories for the measurements such as biceps size, body fat, etc."
},
"measurementCategoriesHelpText": "Categoria de la mesura, com ara «bíceps» o «greix corporal»",
"@measurementCategoriesHelpText": {},
"date": "Data",
"@date": {
"description": "The date of a workout log or body weight entry"
},
"value": "Valor",
"@value": {
"description": "The value of a measurement entry"
},
"difference": "Diferència",
"@difference": {},
"kcal": "kcal",
"@kcal": {
"description": "Energy in a meal in kilocalories, kcal"
},
"proteinShort": "P",
"@proteinShort": {
"description": "The first letter or short name of the word 'Protein', used in overviews"
},
"enterRepetitionsOrWeight": "Ompliu les repeticions o bé el pes per a una de les sèries com a mínim",
"@enterRepetitionsOrWeight": {
"description": "Error message when the user hasn't filled in the forms for exercise sets"
},
"fatShort": "G",
"@fatShort": {
"description": "The first letter or short name of the word 'Fat', used in overviews"
},
"saturatedFat": "Greix saturat",
"@saturatedFat": {},
"noWeightEntries": "No teniu cap entrada de pes",
"@noWeightEntries": {
"description": "Message shown when the user has no logged weight entries"
},
"fibres": "Fibra",
"@fibres": {},
"sodium": "Sodi",
"@sodium": {},
"delete": "Esborra",
"@delete": {},
"aboutSourceText": "Obtingueu el codi font d'aquesta aplicació i el seu servidor a Github",
"@aboutSourceText": {
"description": "Text for source code section in the about dialog"
},
"confirmDelete": "Esteu segur de voler esborrar '{toDelete}'?",
"@confirmDelete": {
"description": "Confirmation text before the user deletes an object",
"type": "text",
"placeholders": {
"toDelete": {}
}
},
"goToToday": "Vés a avui",
"@goToToday": {
"description": "Label on button to jump back to 'today' in the calendar widget"
},
"selectExercise": "Seleccioneu un exercici",
"@selectExercise": {
"description": "Error message when the user hasn't selected an exercise in the form"
},
"enterCharacters": "Introduïu entre {min} i {max} caràcters",
"@enterCharacters": {
"description": "Error message when the user hasn't entered the correct number of characters in a form",
"type": "text",
"placeholders": {
"min": {},
"max": {}
}
},
"recentlyUsedIngredients": "Ingredients afegits recentment",
"@recentlyUsedIngredients": {
"description": "A message when a user adds a new ingredient to a meal."
},
"gallery": "Galeria",
"@gallery": {},
"addImage": "Afegeix imatge",
"@addImage": {},
"dataCopied": "Dades copiades a una nova entrada",
"@dataCopied": {
"description": "Snackbar message to show on copying data to a new log entry"
},
"appUpdateContent": "Aquesta versió de l'app no és compatible amb el servidor, actualitzeu l'aplicació.",
"@appUpdateContent": {}
}

View File

@@ -478,7 +478,7 @@
}, },
"percentEnergy": "Prozent der Energie", "percentEnergy": "Prozent der Energie",
"@percentEnergy": {}, "@percentEnergy": {},
"difference": "Unterschied", "difference": "Differenz",
"@difference": {}, "@difference": {},
"logged": "Protokolliert", "logged": "Protokolliert",
"@logged": { "@logged": {

View File

@@ -507,5 +507,43 @@
"description": "Snackbar message to show on copying data to a new log entry" "description": "Snackbar message to show on copying data to a new log entry"
}, },
"appUpdateTitle" : "Update needed", "appUpdateTitle" : "Update needed",
"appUpdateContent" : "This version of the app is not compatible with the server, please update your application." "appUpdateContent" : "This version of the app is not compatible with the server, please update your application.",
"productFound": "Product found",
"@productFound": {
"description": "Header label for dialog when product is found with barcode"
},
"productFoundDescription": "The barcode corresponds to this product: {productName}. Do you want to continue?",
"@productFoundDescription": {
"description": "Dialog info when product is found with barcode",
"type": "text",
"placeholders": {
"productName": {}
}
},
"productNotFound": "Product not found",
"@productNotFound": {
"description": "Header label for dialog when product is not found with barcode"
},
"productNotFoundDescription": "The product with the scanned barcode {barcode} was not found in the wger database",
"@productNotFoundDescription": {
"description": "Dialog info when product is not found with barcode",
"type": "text",
"placeholders": {
"barcode": {}
}
},
"scanBarcode": "Scan barcode",
"@scanBarcode": {
"description": "Label for scan barcode button"
},
"close": "Close",
"@close": {
"description": "Translation for close"
}
} }

View File

@@ -19,7 +19,7 @@
"@enterValidNumber": { "@enterValidNumber": {
"description": "Error message when the user has submitted an invalid number (e.g. '3,.,.,.')" "description": "Error message when the user has submitted an invalid number (e.g. '3,.,.,.')"
}, },
"nrOfSets": "Número de series: {nrOfSets}", "nrOfSets": "Series por ejercicio: {nrOfSets}",
"@nrOfSets": { "@nrOfSets": {
"description": "Label shown on the slider where the user selects the nr of sets", "description": "Label shown on the slider where the user selects the nr of sets",
"type": "text", "type": "text",
@@ -222,7 +222,7 @@
"@gymMode": { "@gymMode": {
"description": "Label when starting the gym mode" "description": "Label when starting the gym mode"
}, },
"selectExercises": "Si buscas realizar una súper serie, puedes buscar algunos ejercicios, estos estarán agrupados juntos", "selectExercises": "Si buscas realizar una superserie, puedes buscar algunos ejercicios, estos estarán agrupados juntos",
"@selectExercises": {}, "@selectExercises": {},
"newSet": "Nueva serie", "newSet": "Nueva serie",
"@newSet": { "@newSet": {
@@ -479,5 +479,52 @@
"fatShort": "G", "fatShort": "G",
"@fatShort": { "@fatShort": {
"description": "The first letter or short name of the word 'Fat', used in overviews" "description": "The first letter or short name of the word 'Fat', used in overviews"
} },
"logIngredient": "Guardar en el diario nutricional",
"@logIngredient": {},
"plateCalculatorNotDivisible": "No se puede obtener el peso con las placas disponbles",
"@plateCalculatorNotDivisible": {
"description": "Error message when the current weight is not reachable with plates (e.g. 33.1 kg)"
},
"plateCalculator": "Placas",
"@plateCalculator": {
"description": "Label used for the plate calculator in the gym mode"
},
"searchIngredient": "Buscar ingrediente",
"@searchIngredient": {
"description": "Label on ingredient search form"
},
"supersetWith": "en conjunto con",
"@supersetWith": {
"description": "Text used between exercise cards when adding a new set. Translate as something like 'in a superset with'"
},
"rirNotUsed": "Valor RER sin usar",
"@rirNotUsed": {
"description": "Label used in RiR slider when the RiR value is not used/saved for the current setting or log"
},
"planned": "Previsto",
"@planned": {
"description": "Header for the column of 'planned' nutritional values, i.e. what should be eaten"
},
"logged": "Registrado",
"@logged": {
"description": "Header for the column of 'logged' nutritional values, i.e. what was eaten"
},
"gPerBodyKg": "g por kg de peso corporal",
"@gPerBodyKg": {
"description": "Label used for total sums of e.g. calories or similar in grams per Kg of body weight"
},
"setUnitsAndRir": "Unidades de serie y RER",
"@setUnitsAndRir": {
"description": "Label shown on the slider where the user can toggle showing units and RiR",
"type": "text"
},
"recentlyUsedIngredients": "Ingredientes añadidos recientemente",
"@recentlyUsedIngredients": {
"description": "A message when a user adds a new ingredient to a meal."
},
"appUpdateTitle": "Es necesario actualizar",
"@appUpdateTitle": {},
"appUpdateContent": "Esta versión de la aplicación no es compatible con el servidor. Por favor, actualízala.",
"@appUpdateContent": {}
} }

26
lib/l10n/app_hi.arb Normal file
View File

@@ -0,0 +1,26 @@
{
"login": "लोग इन",
"@login": {
"description": "Text for login button"
},
"logout": "लोग आउट",
"@logout": {
"description": "Text for logout button"
},
"useDefaultServer": "डिफ़ॉल्ट सर्वर का प्रयोग करें",
"@useDefaultServer": {
"description": "Toggle button allowing users to switch between the default and a custom wger server"
},
"register": "रजिस्टर",
"@register": {
"description": "Text for registration button"
},
"useCustomServer": "कस्टम सर्वर का प्रयोग करें",
"@useCustomServer": {
"description": "Toggle button allowing users to switch between the default and a custom wger server"
},
"invalidUrl": "कृपया एक वैलिड URL दीजिये",
"@invalidUrl": {
"description": "Error message when the user enters an invalid URL, e.g. in the login form"
}
}

View File

@@ -268,5 +268,7 @@
"logout": "Logg ut", "logout": "Logg ut",
"@logout": { "@logout": {
"description": "Text for logout button" "description": "Text for logout button"
} },
"difference": "Forskjell",
"@difference": {}
} }

524
lib/l10n/app_ru.arb Normal file
View File

@@ -0,0 +1,524 @@
{
"useCustomServer": "Использовать пользовательский сервер",
"@useCustomServer": {
"description": "Toggle button allowing users to switch between the default and a custom wger server"
},
"email": "Адрес электронной почты",
"@email": {},
"labelWorkoutPlans": "Планы тренировок",
"@labelWorkoutPlans": {
"description": "Title for screen workout plans"
},
"registerInstead": "Зарегистроваться вместо этого",
"@registerInstead": {},
"loginInstead": "Войти вместо этого",
"@loginInstead": {},
"labelDashboard": "Панель управления",
"@labelDashboard": {
"description": "Title for screen dashboard"
},
"muscles": "Мышцы",
"@muscles": {
"description": "(main) muscles trained by an exercise"
},
"reps": "Повтор",
"@reps": {
"description": "Shorthand for repetitions, used when space constraints are tighter"
},
"rir": "ПвЗ",
"@rir": {
"description": "Shorthand for Repetitions In Reserve"
},
"rirNotUsed": "ПвЗ не используется",
"@rirNotUsed": {
"description": "Label used in RiR slider when the RiR value is not used/saved for the current setting or log"
},
"notes": "Заметки",
"@notes": {
"description": "Personal notes, e.g. for a workout session"
},
"workoutSession": "Тренировка",
"@workoutSession": {
"description": "A (logged) workout session"
},
"newDay": "Новый день",
"@newDay": {},
"selectExercises": "Если вы хотите сделать суперсет, вы можете найти несколько упражнений, они будут сгруппированы вместе",
"@selectExercises": {},
"plateCalculator": "Пластины",
"@plateCalculator": {
"description": "Label used for the plate calculator in the gym mode"
},
"save": "Сохранить",
"@save": {},
"logMeal": "Записать это блюдо",
"@logMeal": {},
"nutritionalPlan": "План питания",
"@nutritionalPlan": {},
"noNutritionalPlans": "У вас нет планов питания",
"@noNutritionalPlans": {
"description": "Message shown when the user has no nutritional plans"
},
"measurementEntriesHelpText": "Единица, используемая для измерения в категории, например 'см' или '%'",
"@measurementEntriesHelpText": {},
"value": "Значение",
"@value": {
"description": "The value of a measurement entry"
},
"time": "Время",
"@time": {
"description": "The time of a meal or workout"
},
"timeStart": "Время начала",
"@timeStart": {
"description": "The starting time of a workout"
},
"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": {},
"logged": "Записано",
"@logged": {
"description": "Header for the column of 'logged' nutritional values, i.e. what was eaten"
},
"difference": "Разница",
"@difference": {},
"kJ": "кДж",
"@kJ": {
"description": "Energy in a meal in kilo joules, kJ"
},
"g": "г",
"@g": {
"description": "Abbreviation for gram"
},
"goToDetailPage": "Перейти на страницу сведений",
"@goToDetailPage": {},
"aboutSourceTitle": "Исходный код",
"@aboutSourceTitle": {
"description": "Title for source code section in the about dialog"
},
"aboutContactUsTitle": "Скажите привет!",
"@aboutContactUsTitle": {
"description": "Title for contact us section in the about dialog"
},
"aboutTranslationTitle": "Перевод",
"@aboutTranslationTitle": {
"description": "Title for translation section in the about dialog"
},
"calendar": "Календарь",
"@calendar": {},
"enterValue": "Пожалуйста, введите значение",
"@enterValue": {
"description": "Error message when the user hasn't entered a value on a required field"
},
"selectExercise": "Пожалуйста, выберите упражнение",
"@selectExercise": {
"description": "Error message when the user hasn't selected an exercise in the form"
},
"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": {}
}
},
"selectIngredient": "Пожалуйста, выберите ингредиент",
"@selectIngredient": {
"description": "Error message when the user hasn't selected an ingredient from the autocompleter"
},
"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"
},
"takePicture": "Сфотографировать",
"@takePicture": {},
"chooseFromLibrary": "Выбрать из библиотеки фотографий",
"@chooseFromLibrary": {},
"gallery": "Галерея",
"@gallery": {},
"addImage": "Добавить изображение",
"@addImage": {},
"appUpdateTitle": "Требуется обновление",
"@appUpdateTitle": {},
"passwordTooShort": "Пароль слишком короткий",
"@passwordTooShort": {
"description": "Error message when the user a password that is too short"
},
"category": "Категория",
"@category": {
"description": "Category for an exercise, ingredient, etc."
},
"noWorkoutPlans": "У вас нет планов тренировок",
"@noWorkoutPlans": {
"description": "Message shown when the user has no workout plans"
},
"login": "Войти",
"@login": {
"description": "Text for login button"
},
"register": "Зарегистрироваться",
"@register": {
"description": "Text for registration button"
},
"password": "Пароль",
"@password": {},
"logout": "Выйти",
"@logout": {
"description": "Text for logout button"
},
"useDefaultServer": "Использовать сервер по умолчанию",
"@useDefaultServer": {
"description": "Toggle button allowing users to switch between the default and a custom wger server"
},
"invalidUrl": "Пожалуйста, введите действительный URL",
"@invalidUrl": {
"description": "Error message when the user enters an invalid URL, e.g. in the login form"
},
"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": {},
"invalidEmail": "Пожалуйста, введите действительный адрес электронной почты",
"@invalidEmail": {
"description": "Error message when the user enters an invalid email"
},
"username": "Имя пользователя",
"@username": {},
"invalidUsername": "Пожалуйста, введите действительное имя пользователя",
"@invalidUsername": {
"description": "Error message when the user enters an invalid 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"
},
"labelBottomNavWorkout": "Тренировка",
"@labelBottomNavWorkout": {
"description": "Label used in bottom navigation, use a short word"
},
"customServerUrl": "URL-адрес экземпляра wger",
"@customServerUrl": {
"description": "Label in the form where the users can enter their own wger instance"
},
"labelBottomNavNutrition": "Питание",
"@labelBottomNavNutrition": {
"description": "Label used in bottom navigation, use a short word"
},
"labelWorkoutPlan": "План тренировки",
"@labelWorkoutPlan": {
"description": "Title for screen workout plan"
},
"labelWorkoutLogs": "Журналы тренировок",
"@labelWorkoutLogs": {
"description": "(Workout) logs"
},
"exercise": "Упражнение",
"@exercise": {
"description": "An exercise for a workout"
},
"successfullySaved": "Сохранено",
"@successfullySaved": {
"description": "Message when an item was successfully saved"
},
"successfullyDeleted": "Удалено",
"@successfullyDeleted": {
"description": "Message when an item was successfully deleted"
},
"searchExercise": "Найдите упражнение, которое нужно добавить",
"@searchExercise": {
"description": "Label on set form. Selected exercises are added to the set"
},
"equipment": "Оборудование",
"@equipment": {
"description": "Equipment needed to perform an exercise"
},
"musclesSecondary": "Вторичные мышцы",
"@musclesSecondary": {
"description": "secondary muscles trained by an exercise"
},
"newWorkout": "Новый план тренировок",
"@newWorkout": {
"description": "Header when adding a new workout"
},
"dayDescriptionHelp": "Описание того, что делается в этот день (например, 'день подтягиваний') или какие части тела тренируются (например, 'грудь и плечи')",
"@dayDescriptionHelp": {},
"weightUnit": "Единица веса",
"@weightUnit": {},
"repetitionUnit": "Единица повторения",
"@repetitionUnit": {},
"repetitions": "Повторений",
"@repetitions": {
"description": "Repetitions for an exercise set"
},
"comment": "Комментарий",
"@comment": {
"description": "Comment, additional information"
},
"impression": "Впечатление",
"@impression": {
"description": "General impression (e.g. for a workout session) such as good, bad, etc."
},
"set": "Сет",
"@set": {
"description": "A set in a workout plan"
},
"gymMode": "Режим тренажерного зала",
"@gymMode": {
"description": "Label when starting 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"
},
"logHelpEntriesUnits": "Обратите внимание, что в таблицу заносятся только записи с единицей веса (кг или фунты) и повторениями, другие комбинации, такие как время или до отказа, здесь не учитываются.",
"@logHelpEntriesUnits": {},
"pause": "Пауза",
"@pause": {
"description": "Noun, not an imperative! Label used for the pause when using the gym mode"
},
"plateCalculatorNotDivisible": "Невозможно достичь веса с имеющимися пластинами",
"@plateCalculatorNotDivisible": {
"description": "Error message when the current weight is not reachable with plates (e.g. 33.1 kg)"
},
"todaysWorkout": "Ваша тренировка сегодня",
"@todaysWorkout": {},
"logHelpEntries": "Если в один день есть несколько записей с одинаковым количеством повторений, но разными весами, то на диаграмме отображается только запись с большим весом.",
"@logHelpEntries": {},
"description": "Описание",
"@description": {},
"weight": "Вес",
"@weight": {
"description": "The weight of a workout log or body weight entry"
},
"name": "Имя",
"@name": {
"description": "Name for a workout or nutritional plan"
},
"addIngredient": "Добавить ингредиент",
"@addIngredient": {},
"nutritionalPlans": "Планы питания",
"@nutritionalPlans": {},
"timeStartAhead": "Время начала не может быть раньше времени окончания",
"@timeStartAhead": {},
"logIngredient": "Сохранить в дневнике питания",
"@logIngredient": {},
"searchIngredient": "Поиск ингредиента",
"@searchIngredient": {
"description": "Label on ingredient search form"
},
"nutritionalDiary": "Дневник питания",
"@nutritionalDiary": {},
"measurements": "Измерения",
"@measurements": {
"description": "Categories for the measurements such as biceps size, body fat, etc."
},
"anErrorOccurred": "Произошла ошибка!",
"@anErrorOccurred": {},
"measurement": "Измерение",
"@measurement": {},
"measurementCategoriesHelpText": "Категория измерения, например 'бицепс' или 'жир'",
"@measurementCategoriesHelpText": {},
"date": "Дата",
"@date": {
"description": "The date of a workout log or body weight entry"
},
"start": "Начать",
"@start": {
"description": "Label on button to start the gym mode (i.e., an imperative)"
},
"timeEnd": "Время окончания",
"@timeEnd": {
"description": "The end time of a workout"
},
"kcal": "ккал",
"@kcal": {
"description": "Energy in a meal in kilocalories, kcal"
},
"edit": "Редактировать",
"@edit": {},
"delete": "Удалить",
"@delete": {},
"confirmDelete": "Вы уверены, что хотите удалить '{toDelete}'?",
"@confirmDelete": {
"description": "Confirmation text before the user deletes an object",
"type": "text",
"placeholders": {
"toDelete": {}
}
},
"setUnitsAndRir": "Установите единицы измерения и ПвЗ",
"@setUnitsAndRir": {
"description": "Label shown on the slider where the user can toggle showing units and RiR",
"type": "text"
},
"planned": "Запланировано",
"@planned": {
"description": "Header for the column of 'planned' nutritional values, i.e. what should be eaten"
},
"fatShort": "Ж",
"@fatShort": {
"description": "The first letter or short name of the word 'Fat', used in overviews"
},
"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"
},
"fibres": "Клетчатка",
"@fibres": {},
"percentEnergy": "Процент энергии",
"@percentEnergy": {},
"proteinShort": "Б",
"@proteinShort": {
"description": "The first letter or short name of the word 'Protein', used in overviews"
},
"carbohydratesShort": "У",
"@carbohydratesShort": {
"description": "The first letter or short name of the word 'Carbohydrates', used in overviews"
},
"sugars": "Сахар",
"@sugars": {},
"saturatedFat": "Насыщенные жиры",
"@saturatedFat": {},
"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.)"
},
"noWeightEntries": "У вас нет записей о весе",
"@noWeightEntries": {
"description": "Message shown when the user has no logged weight entries"
},
"supersetWith": "суперсет с",
"@supersetWith": {
"description": "Text used between exercise cards when adding a new set. Translate as something like 'in a superset with'"
},
"newEntry": "Новая запись",
"@newEntry": {
"description": "Title when adding a new entry such as a weight or log entry"
},
"loadingText": "Загрузка…",
"@loadingText": {
"description": "Text to show when entries are being loaded in the background: Loading..."
},
"newNutritionalPlan": "Новый план питания",
"@newNutritionalPlan": {},
"toggleDetails": "Переключить сведения",
"@toggleDetails": {
"description": "Switch to toggle detail / overview"
},
"aboutDescription": "Спасибо за использование wger! wger - это совместный проект с открытым исходным кодом, созданный фитнес энтузиастами со всего мира.",
"@aboutDescription": {
"description": "Text in the about dialog"
},
"aboutContactUsText": "Если вы хотите пообщаться с нами, зайдите на сервер Discord и свяжитесь с нами",
"@aboutContactUsText": {
"description": "Text for contact us section in the about dialog"
},
"aboutTranslationText": "Это приложение переведено на weblate. Если вы тоже хотите помочь, нажмите на ссылку и начните переводить",
"@aboutTranslationText": {
"description": "Text for translation 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"
},
"enterValidNumber": "Пожалуйста, введите правильное число",
"@enterValidNumber": {
"description": "Error message when the user has submitted an invalid number (e.g. '3,.,.,.')"
},
"aboutBugsText": "Свяжитесь с нами, если что-то пошло не так или если вам кажется, что какой-то функции не хватает.",
"@aboutBugsText": {
"description": "Text for bugs 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"
},
"optionsLabel": "Опции",
"@optionsLabel": {
"description": "Label for the popup with general app options"
},
"sameRepetitions": "Если вы делаете одинаковые повторения и вес для всех сетов, вы можете просто заполнить одну строку. Например, для 4 сетов просто введите 10 повторений, это автоматически превратится в \"4 x 10\".",
"@sameRepetitions": {},
"newSet": "Новый сет",
"@newSet": {
"description": "Header when adding a new set to a workout day"
},
"dataCopied": "Данные копируются в новую запись",
"@dataCopied": {
"description": "Snackbar message to show on copying data to a new log entry"
},
"appUpdateContent": "Эта версия приложения не совместима с сервером, пожалуйста, обновите ваше приложение.",
"@appUpdateContent": {},
"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": {}
}
},
"addSet": "Добавить сет",
"@addSet": {
"description": "Label for the button that adds a set (to a workout day)"
},
"addMeal": "Добавить блюдо",
"@addMeal": {},
"mealLogged": "Блюдо записано в дневник",
"@mealLogged": {},
"protein": "Белки",
"@protein": {},
"carbohydrates": "Углеводы",
"@carbohydrates": {},
"fat": "Жиры",
"@fat": {}
}

View File

@@ -358,5 +358,9 @@
"saturatedFat": "Doymuş yağ", "saturatedFat": "Doymuş yağ",
"@saturatedFat": {}, "@saturatedFat": {},
"fibres": "Lif", "fibres": "Lif",
"@fibres": {} "@fibres": {},
"delete": "Sil",
"@delete": {},
"edit": "Düzenle",
"@edit": {}
} }

View File

@@ -25,6 +25,10 @@ class Ingredient {
@JsonKey(required: true) @JsonKey(required: true)
final int id; final int id;
/// Barcode of the product
@JsonKey(required: true)
final String code;
/// Name of the product /// Name of the product
@JsonKey(required: true) @JsonKey(required: true)
final String name; final String name;
@@ -66,6 +70,7 @@ class Ingredient {
const Ingredient({ const Ingredient({
required this.id, required this.id,
required this.code,
required this.name, required this.name,
required this.creationDate, required this.creationDate,
required this.energy, required this.energy,

View File

@@ -11,6 +11,7 @@ Ingredient _$IngredientFromJson(Map<String, dynamic> json) {
json, json,
requiredKeys: const [ requiredKeys: const [
'id', 'id',
'code',
'name', 'name',
'creation_date', 'creation_date',
'energy', 'energy',
@@ -25,6 +26,7 @@ Ingredient _$IngredientFromJson(Map<String, dynamic> json) {
); );
return Ingredient( return Ingredient(
id: json['id'] as int, id: json['id'] as int,
code: json['code'] as String,
name: json['name'] as String, name: json['name'] as String,
creationDate: DateTime.parse(json['creation_date'] as String), creationDate: DateTime.parse(json['creation_date'] as String),
energy: json['energy'] as int, energy: json['energy'] as int,
@@ -40,6 +42,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, 'id': instance.id,
'code': instance.code,
'name': instance.name, 'name': instance.name,
'creation_date': toDate(instance.creationDate), 'creation_date': toDate(instance.creationDate),
'energy': instance.energy, 'energy': instance.energy,

View File

@@ -326,6 +326,27 @@ class NutritionPlansProvider extends WgerBaseProvider with ChangeNotifier {
return json.decode(utf8.decode(response.bodyBytes))['suggestions'] as List<dynamic>; return json.decode(utf8.decode(response.bodyBytes))['suggestions'] as List<dynamic>;
} }
/// Searches for an ingredient with code
Future<Ingredient?> searchIngredientWithCode(String code) async {
if (code.isEmpty) {
return null;
}
// Send the request
final data = await fetch(
makeUrl(
_ingredientPath,
query: {'code': code},
),
);
if (data["count"] == 0) {
return null;
} else {
return Ingredient.fromJson(data['results'][0]);
}
}
/// Log meal to nutrition diary /// Log meal to nutrition diary
Future<void> logMealToDiary(Meal meal) async { Future<void> logMealToDiary(Meal meal) async {
for (final item in meal.mealItems) { for (final item in meal.mealItems) {

View File

@@ -352,9 +352,11 @@ class _DashboardWorkoutWidgetState extends State<DashboardWorkoutWidget> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
MutedText( Expanded(
day.getDaysText, child: MutedText(
textAlign: TextAlign.right, day.getDaysText,
textAlign: TextAlign.right,
),
), ),
IconButton( IconButton(
icon: const Icon(Icons.play_arrow), icon: const Icon(Icons.play_arrow),

View File

@@ -17,6 +17,7 @@
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/exceptions/http_exception.dart';
@@ -116,17 +117,25 @@ class MealItemForm extends StatelessWidget {
final Meal _meal; final Meal _meal;
late final MealItem _mealItem; late final MealItem _mealItem;
final List<MealItem> _listMealItems; final List<MealItem> _listMealItems;
late String _barcode;
late bool _test;
final _form = GlobalKey<FormState>(); final _form = GlobalKey<FormState>();
final _ingredientIdController = TextEditingController(); final _ingredientIdController = TextEditingController();
final _ingredientController = TextEditingController(); final _ingredientController = TextEditingController();
final _amountController = TextEditingController(); final _amountController = TextEditingController();
MealItemForm(this._meal, this._listMealItems, [mealItem]) { MealItemForm(this._meal, this._listMealItems, [mealItem, code, test]) {
_mealItem = mealItem ?? MealItem.empty(); _mealItem = mealItem ?? MealItem.empty();
_test = test ?? false;
_barcode = code ?? '';
_mealItem.mealId = _meal.id!; _mealItem.mealId = _meal.id!;
} }
TextEditingController get ingredientIdController => _ingredientIdController;
MealItem get mealItem => _mealItem;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final String unit = AppLocalizations.of(context).g; final String unit = AppLocalizations.of(context).g;
@@ -136,8 +145,15 @@ class MealItemForm extends StatelessWidget {
key: _form, key: _form,
child: Column( child: Column(
children: [ children: [
IngredientTypeahead(_ingredientIdController, _ingredientController), IngredientTypeahead(
_ingredientIdController,
_ingredientController,
true,
_barcode,
_test,
),
TextFormField( TextFormField(
key: const Key('field-weight'),
decoration: InputDecoration(labelText: AppLocalizations.of(context).weight), decoration: InputDecoration(labelText: AppLocalizations.of(context).weight),
controller: _amountController, controller: _amountController,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
@@ -155,6 +171,7 @@ class MealItemForm extends StatelessWidget {
}, },
), ),
ElevatedButton( ElevatedButton(
key: const Key(SUBMIT_BUTTON_KEY_NAME),
child: Text(AppLocalizations.of(context).save), child: Text(AppLocalizations.of(context).save),
onPressed: () async { onPressed: () async {
if (!_form.currentState!.validate()) { if (!_form.currentState!.validate()) {
@@ -235,7 +252,13 @@ class IngredientLogForm extends StatelessWidget {
key: _form, key: _form,
child: Column( child: Column(
children: [ children: [
IngredientTypeahead(_ingredientIdController, _ingredientController), IngredientTypeahead(
_ingredientIdController,
_ingredientController,
true,
'',
false,
),
TextFormField( TextFormField(
decoration: InputDecoration(labelText: AppLocalizations.of(context).weight), decoration: InputDecoration(labelText: AppLocalizations.of(context).weight),
controller: _amountController, controller: _amountController,

View File

@@ -17,22 +17,44 @@
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:wger/helpers/ui.dart';
import 'package:wger/providers/nutrition.dart'; import 'package:wger/providers/nutrition.dart';
class IngredientTypeahead extends StatefulWidget { class IngredientTypeahead extends StatefulWidget {
final TextEditingController _ingredientController; final TextEditingController _ingredientController;
final TextEditingController _ingredientIdController; final TextEditingController _ingredientIdController;
late String? _barcode;
late final bool? _test;
final bool _showScanner;
const IngredientTypeahead(this._ingredientIdController, this._ingredientController); IngredientTypeahead(this._ingredientIdController, this._ingredientController, this._showScanner,
[this._barcode, this._test]);
@override @override
_IngredientTypeaheadState createState() => _IngredientTypeaheadState(); _IngredientTypeaheadState createState() => _IngredientTypeaheadState();
} }
Future<String> scanBarcode(BuildContext context) async {
String barcode;
try {
barcode = await FlutterBarcodeScanner.scanBarcode(
'#ff6666', AppLocalizations.of(context).close, true, ScanMode.BARCODE);
if (barcode.compareTo('-1') == 0) {
return '';
}
} on PlatformException {
return '';
}
return barcode;
}
class _IngredientTypeaheadState extends State<IngredientTypeahead> { class _IngredientTypeaheadState extends State<IngredientTypeahead> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -40,8 +62,9 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
textFieldConfiguration: TextFieldConfiguration( textFieldConfiguration: TextFieldConfiguration(
controller: widget._ingredientController, controller: widget._ingredientController,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.search),
labelText: AppLocalizations.of(context).searchIngredient, labelText: AppLocalizations.of(context).searchIngredient,
suffixIcon: const Icon(Icons.search), suffixIcon: widget._showScanner ? scanButton() : null,
), ),
), ),
suggestionsCallback: (pattern) async { suggestionsCallback: (pattern) async {
@@ -71,4 +94,75 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
}, },
); );
} }
Widget scanButton() {
return IconButton(
key: const Key('scan-button'),
onPressed: () async {
try {
if (!widget._test!) {
widget._barcode = await scanBarcode(context);
}
if (widget._barcode!.isNotEmpty) {
final result = await Provider.of<NutritionPlansProvider>(context, listen: false)
.searchIngredientWithCode(widget._barcode!);
if (result != null) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
key: const Key('found-dialog'),
title: Text(AppLocalizations.of(context).productFound),
content:
Text(AppLocalizations.of(context).productFoundDescription(result.name)),
actions: [
TextButton(
key: const Key('found-dialog-confirm-button'),
child: Text(MaterialLocalizations.of(context).continueButtonLabel),
onPressed: () {
widget._ingredientController.text = result.name;
widget._ingredientIdController.text = result.id.toString();
Navigator.of(ctx).pop();
},
),
TextButton(
key: const Key('found-dialog-close-button'),
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
onPressed: () {
Navigator.of(ctx).pop();
},
)
],
),
);
} else {
//nothing is matching barcode
showDialog(
context: context,
builder: (ctx) => AlertDialog(
key: const Key('notFound-dialog'),
title: Text(AppLocalizations.of(context).productNotFound),
content: Text(
AppLocalizations.of(context).productNotFoundDescription(widget._barcode!),
),
actions: [
TextButton(
key: const Key('notFound-dialog-close-button'),
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
onPressed: () {
Navigator.of(ctx).pop();
},
)
],
),
);
}
}
} catch (e) {
showErrorDialog(e, context);
}
},
icon: Image.asset('assets/images/barcode_scanner_icon.png'));
}
} }

View File

@@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared name: _fe_analyzer_shared
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "31.0.0" version: "33.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.0" version: "3.1.0"
android_metadata: android_metadata:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -28,7 +28,7 @@ packages:
name: archive name: archive
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.6" version: "3.1.8"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -42,7 +42,7 @@ packages:
name: async name: async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.2" version: "2.8.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -56,7 +56,7 @@ packages:
name: build name: build
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.2.1"
build_config: build_config:
dependency: transitive dependency: transitive
description: description:
@@ -77,21 +77,21 @@ packages:
name: build_resolvers name: build_resolvers
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.0.6"
build_runner: build_runner:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: build_runner name: build_runner
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.5" version: "2.1.7"
build_runner_core: build_runner_core:
dependency: transitive dependency: transitive
description: description:
name: build_runner_core name: build_runner_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "7.2.2" version: "7.2.3"
built_collection: built_collection:
dependency: transitive dependency: transitive
description: description:
@@ -105,7 +105,7 @@ packages:
name: built_value name: built_value
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "8.1.3" version: "8.1.4"
camera: camera:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -119,7 +119,7 @@ packages:
name: camera_platform_interface name: camera_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.4"
camera_web: camera_web:
dependency: transitive dependency: transitive
description: description:
@@ -140,7 +140,7 @@ packages:
name: characters name: characters
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.1.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@@ -182,7 +182,7 @@ packages:
name: chewie_audio name: chewie_audio
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
cider: cider:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -259,7 +259,7 @@ packages:
name: dart_style name: dart_style
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
equatable: equatable:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -300,6 +300,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_barcode_scanner:
dependency: "direct main"
description:
name: flutter_barcode_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
flutter_calendar_carousel: flutter_calendar_carousel:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -320,7 +327,7 @@ packages:
name: flutter_keyboard_visibility name: flutter_keyboard_visibility
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.1.0" version: "5.1.1"
flutter_keyboard_visibility_platform_interface: flutter_keyboard_visibility_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -461,7 +468,7 @@ packages:
name: image name: image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.0" version: "3.1.1"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -475,14 +482,14 @@ packages:
name: image_picker_for_web name: image_picker_for_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.4" version: "2.1.5"
image_picker_platform_interface: image_picker_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: image_picker_platform_interface name: image_picker_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.4.1" version: "2.4.3"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -517,7 +524,7 @@ packages:
name: json_serializable name: json_serializable
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.1.1" version: "6.1.4"
klizma: klizma:
dependency: transitive dependency: transitive
description: description:
@@ -559,7 +566,7 @@ packages:
name: matcher name: matcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.11" version: "0.12.10"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -580,7 +587,7 @@ packages:
name: mockito name: mockito
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.16" version: "5.0.17"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@@ -643,21 +650,21 @@ packages:
name: path_provider_linux name: path_provider_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.4" version: "2.1.5"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.3"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.4" version: "2.0.5"
pedantic: pedantic:
dependency: transitive dependency: transitive
description: description:
@@ -685,7 +692,7 @@ packages:
name: plugin_platform_interface name: plugin_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.1.2"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@@ -706,7 +713,7 @@ packages:
name: provider name: provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.1" version: "6.0.2"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -741,35 +748,35 @@ packages:
name: rive name: rive
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.7.33" version: "0.8.1"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.11" version: "2.0.12"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.9" version: "2.0.10"
shared_preferences_ios: shared_preferences_ios:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_ios name: shared_preferences_ios
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.8" version: "2.0.9"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_linux name: shared_preferences_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.3" version: "2.0.4"
shared_preferences_macos: shared_preferences_macos:
dependency: transitive dependency: transitive
description: description:
@@ -790,14 +797,14 @@ packages:
name: shared_preferences_web name: shared_preferences_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.0.3"
shared_preferences_windows: shared_preferences_windows:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_windows name: shared_preferences_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.3" version: "2.0.4"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@@ -830,14 +837,14 @@ packages:
name: source_gen name: source_gen
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.2.1"
source_helper: source_helper:
dependency: transitive dependency: transitive
description: description:
name: source_helper name: source_helper
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.1"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@@ -893,7 +900,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.3" version: "0.4.2"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@@ -921,21 +928,21 @@ packages:
name: url_launcher name: url_launcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.17" version: "6.0.18"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.13" version: "6.0.14"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.13" version: "6.0.14"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@@ -956,14 +963,14 @@ packages:
name: url_launcher_platform_interface name: url_launcher_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.4" version: "2.0.5"
url_launcher_web: url_launcher_web:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.0.6"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
@@ -977,7 +984,7 @@ packages:
name: vector_math name: vector_math
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.0"
version: version:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -998,21 +1005,21 @@ packages:
name: video_player name: video_player
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.7" version: "2.2.11"
video_player_platform_interface: video_player_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: video_player_platform_interface name: video_player_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.2.0" version: "5.0.1"
video_player_web: video_player_web:
dependency: transitive dependency: transitive
description: description:
name: video_player_web name: video_player_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.4" version: "2.0.6"
wakelock: wakelock:
dependency: transitive dependency: transitive
description: description:
@@ -1075,14 +1082,14 @@ packages:
name: webview_flutter_android name: webview_flutter_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.0" version: "2.8.2"
webview_flutter_platform_interface: webview_flutter_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_platform_interface name: webview_flutter_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0" version: "1.8.1"
webview_flutter_wkwebview: webview_flutter_wkwebview:
dependency: transitive dependency: transitive
description: description:
@@ -1096,7 +1103,7 @@ packages:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.3.1" version: "2.3.3"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View File

@@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# github action, the current versions are ignored. # github action, the current versions are ignored.
# - the version number is taken from the git branch release/x.y.z # - the version number is taken from the git branch release/x.y.z
# - the build number is computed by reading the last one from the play store and increased by one # - the build number is computed by reading the last one from the play store and increased by one
version: 1.4.0+24 version: 1.4.1+25
environment: environment:
sdk: '>=2.12.0 <3.0.0' sdk: '>=2.12.0 <3.0.0'
@@ -48,10 +48,11 @@ dependencies:
version: ^2.0.0 version: ^2.0.0
package_info: ^2.0.2 package_info: ^2.0.2
provider: ^6.0.1 provider: ^6.0.1
rive: ^0.7.33 rive: ^0.8.1
shared_preferences: ^2.0.7 shared_preferences: ^2.0.7
table_calendar: ^3.0.2 table_calendar: ^3.0.2
url_launcher: ^6.0.10 url_launcher: ^6.0.10
flutter_barcode_scanner: ^2.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -0,0 +1,15 @@
{"suggestions":[
{
"value": "'n Dry 100% Rapeseed Oil 1 Litre",
"data": {
"id": 61810,
"name": "'n Dry 100% Rapeseed Oil 1 Litre"
}},
{
"value": "100% Apple & Mango",
"data": {
"id": 61972,
"name": "100% Apple & Mango"
}
}]
}

View File

@@ -0,0 +1,25 @@
{
"count":1,
"next":null,
"previous":null,
"results":[
{
"id":9436,
"code":"0013087245950",
"name":" Gâteau double chocolat ",
"creation_date":"2020-12-20",
"update_date":"2020-12-20",
"energy":360,
"protein":"5.000",
"carbohydrates":"45.000",
"carbohydrates_sugar":"27.000",
"fat":"18.000",
"fat_saturated":"4.500",
"fibres":"2.000",
"sodium":"0.356",
"license":5,
"license_author":"Open Food Facts",
"language":12
}
]
}

View File

@@ -0,0 +1,6 @@
{
"count":0,
"next":null,
"previous":null,
"results":[]
}

View File

@@ -1,4 +1,4 @@
// Mocks generated by Mockito 5.0.16 from annotations // Mocks generated by Mockito 5.0.17 from annotations
// in wger/test/gallery/gallery_screen_test.dart. // in wger/test/gallery/gallery_screen_test.dart.
// Do not manually edit this file. // Do not manually edit this file.
@@ -109,8 +109,6 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider {
(super.noSuchMethod(Invocation.method(#deleteRequest, [url, id]), (super.noSuchMethod(Invocation.method(#deleteRequest, [url, id]),
returnValue: Future<_i3.Response>.value(_FakeResponse_3())) as _i6.Future<_i3.Response>); returnValue: Future<_i3.Response>.value(_FakeResponse_3())) as _i6.Future<_i3.Response>);
@override @override
String toString() => super.toString();
@override
void addListener(_i8.VoidCallback? listener) => super void addListener(_i8.VoidCallback? listener) => super
.noSuchMethod(Invocation.method(#addListener, [listener]), returnValueForMissingStub: null); .noSuchMethod(Invocation.method(#addListener, [listener]), returnValueForMissingStub: null);
@override @override

View File

@@ -1,4 +1,4 @@
// Mocks generated by Mockito 5.0.16 from annotations // Mocks generated by Mockito 5.0.17 from annotations
// in wger/test/measurements/measurement_categories_screen_test.dart. // in wger/test/measurements/measurement_categories_screen_test.dart.
// Do not manually edit this file. // Do not manually edit this file.
@@ -98,8 +98,6 @@ class MockMeasurementProvider extends _i1.Mock implements _i4.MeasurementProvide
returnValue: Future<void>.value(), returnValue: Future<void>.value(),
returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>); returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
@override @override
String toString() => super.toString();
@override
void addListener(_i7.VoidCallback? listener) => super void addListener(_i7.VoidCallback? listener) => super
.noSuchMethod(Invocation.method(#addListener, [listener]), returnValueForMissingStub: null); .noSuchMethod(Invocation.method(#addListener, [listener]), returnValueForMissingStub: null);
@override @override

View File

@@ -1,4 +1,4 @@
// Mocks generated by Mockito 5.0.16 from annotations // Mocks generated by Mockito 5.0.17 from annotations
// in wger/test/measurements/measurement_provider_test.dart. // in wger/test/measurements/measurement_provider_test.dart.
// Do not manually edit this file. // Do not manually edit this file.
@@ -72,6 +72,4 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
_i5.Future<_i3.Response> deleteRequest(String? url, int? id) => _i5.Future<_i3.Response> deleteRequest(String? url, int? id) =>
(super.noSuchMethod(Invocation.method(#deleteRequest, [url, id]), (super.noSuchMethod(Invocation.method(#deleteRequest, [url, id]),
returnValue: Future<_i3.Response>.value(_FakeResponse_3())) as _i5.Future<_i3.Response>); returnValue: Future<_i3.Response>.value(_FakeResponse_3())) as _i5.Future<_i3.Response>);
@override
String toString() => super.toString();
} }

View File

@@ -0,0 +1,304 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:http/http.dart' as http;
import 'package:mockito/mockito.dart';
import 'package:provider/provider.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/models/nutrition/ingredient.dart';
import 'package:wger/models/nutrition/meal.dart';
import 'package:wger/models/nutrition/meal_item.dart';
import 'package:wger/models/nutrition/nutritional_plan.dart';
import 'package:wger/providers/nutrition.dart';
import 'package:wger/screens/nutritional_plan_screen.dart';
import 'package:wger/widgets/nutrition/forms.dart';
import '../../test_data/nutritional_plans.dart';
import '../fixtures/fixture_reader.dart';
import '../other/base_provider_test.mocks.dart';
import '../utils.dart';
import 'nutritional_plan_form_test.mocks.dart';
void main() {
Ingredient ingr = Ingredient(
id: 1,
code: '123456787',
name: 'Water',
creationDate: DateTime(2021, 5, 1),
energy: 500,
carbohydrates: 10,
carbohydratesSugar: 2,
protein: 5,
fat: 20,
fatSaturated: 7,
fibres: 12,
sodium: 0.5,
);
var mockNutrition = MockNutritionPlansProvider();
final client = MockClient();
final mockNutritionWithClient = NutritionPlansProvider(testAuthProvider, [], client);
var plan1 = NutritionalPlan.empty();
var meal1 = Meal();
final Uri tUriRightCode = Uri.parse('https://localhost/api/v2/ingredient/?code=123');
final Uri tUriEmptyCode = Uri.parse('https://localhost/api/v2/ingredient/?code=\"%20\"');
final Uri tUriBadCode = Uri.parse('https://localhost/api/v2/ingredient/?code=222');
when(client
.get(tUriRightCode, headers: {'authorization': 'Token FooBar', 'user-agent': 'wger App'}))
.thenAnswer(
(_) => Future.value(http.Response(fixture('search_ingredient_right_code.json'), 200)));
when(client
.get(tUriEmptyCode, headers: {'authorization': 'Token FooBar', 'user-agent': 'wger App'}))
.thenAnswer(
(_) => Future.value(http.Response(fixture('search_ingredient_wrong_code.json'), 200)));
when(client
.get(tUriBadCode, headers: {'authorization': 'Token FooBar', 'user-agent': 'wger App'}))
.thenAnswer(
(_) => Future.value(http.Response(fixture('search_ingredient_wrong_code.json'), 200)));
setUp(() {
plan1 = getNutritionalPlan();
meal1 = plan1.meals.first;
final MealItem mealItem = MealItem(ingredientId: ingr.id, amount: 2);
mockNutrition = MockNutritionPlansProvider();
when(mockNutrition.searchIngredientWithCode('123')).thenAnswer((_) => Future.value(ingr));
when(mockNutrition.searchIngredientWithCode('')).thenAnswer((_) => Future.value(null));
when(mockNutrition.searchIngredientWithCode('222')).thenAnswer((_) => Future.value(null));
when(mockNutrition.searchIngredient(any)).thenAnswer(
(_) => Future.value(json.decode(fixture('ingredient_suggestions')) as List<dynamic>));
when(mockNutrition.addMealItem(any, meal1)).thenAnswer((_) => Future.value(mealItem));
});
Widget createMealItemFormScreen(Meal meal, String code, bool test, {locale = 'en'}) {
final key = GlobalKey<NavigatorState>();
return ChangeNotifierProvider<NutritionPlansProvider>(
create: (context) => mockNutrition,
child: MaterialApp(
locale: Locale(locale),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
navigatorKey: key,
home: Scaffold(
body: MealItemForm(meal, const [], null, code, test),
),
routes: {
NutritionalPlanScreen.routeName: (ctx) => NutritionalPlanScreen(),
},
),
);
}
testWidgets('Test the widgets on the meal item form', (WidgetTester tester) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '', true));
await tester.pumpAndSettle();
expect(find.byType(TypeAheadFormField), findsOneWidget);
expect(find.byType(TextFormField), findsOneWidget);
expect(find.byKey(const Key('scan-button')), findsOneWidget);
expect(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME)), findsOneWidget);
expect(meal1.mealItems.length, 2);
});
group('Test the AlertDialogs for scanning result', () {
testWidgets('with empty code', (WidgetTester tester) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '', true));
await tester.tap(find.byKey(const Key('scan-button')));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsNothing);
});
testWidgets('with correct code', (WidgetTester tester) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true));
await tester.tap(find.byKey(const Key('scan-button')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('found-dialog')), findsOneWidget);
});
testWidgets('with incorrect code', (WidgetTester tester) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '222', true));
await tester.tap(find.byKey(const Key('scan-button')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('notFound-dialog')), findsOneWidget);
});
});
group('Test searchIngredientWithCode() function', () {
test('with correct code', () async {
final Ingredient? ingredient = await mockNutritionWithClient.searchIngredientWithCode('123');
expect(ingredient!.id, 9436);
});
test('with incorrect code', () async {
final Ingredient? ingredient = await mockNutritionWithClient.searchIngredientWithCode('222');
expect(ingredient, null);
});
test('with empty code', () async {
final Ingredient? ingredient = await mockNutritionWithClient.searchIngredientWithCode('');
expect(ingredient, null);
});
});
group('Test weight formfield', () {
testWidgets('add empty weight', (WidgetTester tester) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true));
await tester.enterText(find.byKey(const Key('field-weight')), '');
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME)));
await tester.pumpAndSettle();
expect(find.text('Please enter a valid number'), findsOneWidget);
});
testWidgets('add correct weight type', (WidgetTester tester) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true));
await tester.enterText(find.byKey(const Key('field-weight')), '2');
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME)));
await tester.pumpAndSettle();
expect(find.text('Please enter a valid number'), findsNothing);
});
testWidgets('add incorrect weight type', (WidgetTester tester) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true));
await tester.enterText(find.byKey(const Key('field-weight')), 'test');
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME)));
await tester.pumpAndSettle();
expect(find.text('Please enter a valid number'), findsOneWidget);
});
});
group('Test ingredient found dialog', () {
testWidgets('confirm found ingredient dialog', (WidgetTester tester) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true));
final MealItemForm formScreen = tester.widget(find.byType(MealItemForm));
await tester.tap(find.byKey(const Key('scan-button')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('found-dialog')), findsOneWidget);
await tester.tap(find.byKey(const Key('found-dialog-confirm-button')));
await tester.pumpAndSettle();
expect(formScreen.ingredientIdController.text, '1');
});
testWidgets('close found ingredient dialog', (WidgetTester tester) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true));
await tester.tap(find.byKey(const Key('scan-button')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('found-dialog')), findsOneWidget);
await tester.tap(find.byKey(const Key('found-dialog-close-button')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('found-dialog')), findsNothing);
});
});
group('Test the adding a new item to meal', () {
testWidgets('save empty ingredient', (WidgetTester tester) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true));
await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME)));
await tester.pumpAndSettle();
expect(find.text('Please select an ingredient'), findsOneWidget);
expect(find.text('Please enter a valid number'), findsOneWidget);
});
testWidgets('save ingredient without weight', (WidgetTester tester) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true));
await tester.tap(find.byKey(const Key('scan-button')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('found-dialog')), findsOneWidget);
await tester.tap(find.byKey(const Key('found-dialog-confirm-button')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME)));
await tester.pumpAndSettle();
expect(find.text('Please enter a valid number'), findsOneWidget);
});
testWidgets('save ingredient with incorrect weight input type', (WidgetTester tester) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true));
await tester.tap(find.byKey(const Key('scan-button')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('found-dialog')), findsOneWidget);
await tester.tap(find.byKey(const Key('found-dialog-confirm-button')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME)));
await tester.pumpAndSettle();
expect(find.text('Please enter a valid number'), findsOneWidget);
});
testWidgets('save complete ingredient with correct weight input type',
(WidgetTester tester) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true));
final MealItemForm formScreen = tester.widget(find.byType(MealItemForm));
await tester.tap(find.byKey(const Key('scan-button')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('found-dialog')), findsOneWidget);
await tester.tap(find.byKey(const Key('found-dialog-confirm-button')));
await tester.pumpAndSettle();
expect(formScreen.ingredientIdController.text, '1');
await tester.enterText(find.byKey(const Key('field-weight')), '2');
await tester.pumpAndSettle();
expect(find.byKey(const Key('found-dialog')), findsNothing);
await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME)));
await tester.pumpAndSettle();
expect(formScreen.mealItem.amount, 2);
verify(mockNutrition.addMealItem(any, meal1));
});
});
}

View File

@@ -1,4 +1,4 @@
// Mocks generated by Mockito 5.0.16 from annotations // Mocks generated by Mockito 5.0.17 from annotations
// in wger/test/nutrition/nutritional_plan_form_test.dart. // in wger/test/nutrition/nutritional_plan_form_test.dart.
// Do not manually edit this file. // Do not manually edit this file.
@@ -193,8 +193,6 @@ class MockNutritionPlansProvider extends _i1.Mock implements _i8.NutritionPlansP
(super.noSuchMethod(Invocation.method(#deleteRequest, [url, id]), (super.noSuchMethod(Invocation.method(#deleteRequest, [url, id]),
returnValue: Future<_i3.Response>.value(_FakeResponse_7())) as _i9.Future<_i3.Response>); returnValue: Future<_i3.Response>.value(_FakeResponse_7())) as _i9.Future<_i3.Response>);
@override @override
String toString() => super.toString();
@override
void addListener(_i10.VoidCallback? listener) => super void addListener(_i10.VoidCallback? listener) => super
.noSuchMethod(Invocation.method(#addListener, [listener]), returnValueForMissingStub: null); .noSuchMethod(Invocation.method(#addListener, [listener]), returnValueForMissingStub: null);
@override @override

View File

@@ -1,4 +1,4 @@
// Mocks generated by Mockito 5.0.16 from annotations // Mocks generated by Mockito 5.0.17 from annotations
// in wger/test/other/base_provider_test.dart. // in wger/test/other/base_provider_test.dart.
// Do not manually edit this file. // Do not manually edit this file.
@@ -81,6 +81,4 @@ class MockClient extends _i1.Mock implements _i4.Client {
@override @override
void close() => void close() =>
super.noSuchMethod(Invocation.method(#close, []), returnValueForMissingStub: null); super.noSuchMethod(Invocation.method(#close, []), returnValueForMissingStub: null);
@override
String toString() => super.toString();
} }

View File

@@ -1,4 +1,4 @@
// Mocks generated by Mockito 5.0.16 from annotations // Mocks generated by Mockito 5.0.17 from annotations
// in wger/test/workout/workout_form_test.dart. // in wger/test/workout/workout_form_test.dart.
// Do not manually edit this file. // Do not manually edit this file.
@@ -256,8 +256,6 @@ class MockWorkoutPlansProvider extends _i1.Mock implements _i12.WorkoutPlansProv
Invocation.method(#deleteRequest, [url, id]), Invocation.method(#deleteRequest, [url, id]),
returnValue: Future<_i5.Response>.value(_FakeResponse_11())) as _i13.Future<_i5.Response>); returnValue: Future<_i5.Response>.value(_FakeResponse_11())) as _i13.Future<_i5.Response>);
@override @override
String toString() => super.toString();
@override
void addListener(_i15.VoidCallback? listener) => super void addListener(_i15.VoidCallback? listener) => super
.noSuchMethod(Invocation.method(#addListener, [listener]), returnValueForMissingStub: null); .noSuchMethod(Invocation.method(#addListener, [listener]), returnValueForMissingStub: null);
@override @override

View File

@@ -1,4 +1,4 @@
// Mocks generated by Mockito 5.0.16 from annotations // Mocks generated by Mockito 5.0.17 from annotations
// in wger/test/workout/workout_set_form_test.dart. // in wger/test/workout/workout_set_form_test.dart.
// Do not manually edit this file. // Do not manually edit this file.
@@ -116,8 +116,6 @@ class MockExercisesProvider extends _i1.Mock implements _i5.ExercisesProvider {
(super.noSuchMethod(Invocation.method(#deleteRequest, [url, id]), (super.noSuchMethod(Invocation.method(#deleteRequest, [url, id]),
returnValue: Future<_i3.Response>.value(_FakeResponse_4())) as _i6.Future<_i3.Response>); returnValue: Future<_i3.Response>.value(_FakeResponse_4())) as _i6.Future<_i3.Response>);
@override @override
String toString() => super.toString();
@override
void addListener(_i7.VoidCallback? listener) => super void addListener(_i7.VoidCallback? listener) => super
.noSuchMethod(Invocation.method(#addListener, [listener]), returnValueForMissingStub: null); .noSuchMethod(Invocation.method(#addListener, [listener]), returnValueForMissingStub: null);
@override @override

View File

@@ -25,6 +25,7 @@ import 'package:wger/models/nutrition/nutritional_plan.dart';
final ingredient1 = Ingredient( final ingredient1 = Ingredient(
id: 1, id: 1,
code: '123456787',
name: 'Water', name: 'Water',
creationDate: DateTime(2021, 5, 1), creationDate: DateTime(2021, 5, 1),
energy: 500, energy: 500,
@@ -38,6 +39,7 @@ final ingredient1 = Ingredient(
); );
final ingredient2 = Ingredient( final ingredient2 = Ingredient(
id: 2, id: 2,
code: '123456788',
name: 'Burger soup', name: 'Burger soup',
creationDate: DateTime(2021, 5, 10), creationDate: DateTime(2021, 5, 10),
energy: 25, energy: 25,
@@ -51,6 +53,7 @@ final ingredient2 = Ingredient(
); );
final ingredient3 = Ingredient( final ingredient3 = Ingredient(
id: 3, id: 3,
code: '123456789',
name: 'Broccoli cake', name: 'Broccoli cake',
creationDate: DateTime(2021, 5, 2), creationDate: DateTime(2021, 5, 2),
energy: 1200, energy: 1200,