From 3d2779c2e0b48ab19292ee63193563ac4e24d61b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 13:24:17 +0000 Subject: [PATCH 01/20] Bump package_info_plus from 7.0.0 to 8.0.0 Bumps [package_info_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/package_info_plus) from 7.0.0 to 8.0.0. - [Release notes](https://github.com/fluttercommunity/plus_plugins/releases) - [Commits](https://github.com/fluttercommunity/plus_plugins/commits/package_info_plus-v8.0.0/packages/package_info_plus) --- updated-dependencies: - dependency-name: package_info_plus dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 6990a460..336b1b3c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -904,10 +904,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "2c582551839386fa7ddbc7770658be7c0f87f388a4bff72066478f597c34d17f" + sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "8.0.0" package_info_plus_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bb1e0c6f..8286d1d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,7 +45,7 @@ dependencies: intl: ^0.18.1 json_annotation: ^4.8.1 version: ^3.0.2 - package_info_plus: ^7.0.0 + package_info_plus: ^8.0.0 provider: ^6.1.2 rive: ^0.13.2 shared_preferences: ^2.2.3 From 0bf27084b2209ec98bdfca5cd8de0a8ce0cd4a72 Mon Sep 17 00:00:00 2001 From: Licaon_Kter Date: Mon, 13 May 2024 15:40:36 +0000 Subject: [PATCH 02/20] Rename logo.png to icon.png --- .../android/en-US/images/{logo.png => icon.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename fastlane/metadata/android/en-US/images/{logo.png => icon.png} (100%) diff --git a/fastlane/metadata/android/en-US/images/logo.png b/fastlane/metadata/android/en-US/images/icon.png similarity index 100% rename from fastlane/metadata/android/en-US/images/logo.png rename to fastlane/metadata/android/en-US/images/icon.png From ba712c27cb902212f54c2c104acfae7d324cbf3e Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 08:58:57 +0200 Subject: [PATCH 03/20] cleanup for rive workaround --- linux/flutter/generated_plugin_registrant.cc | 4 ---- linux/flutter/generated_plugins.cmake | 1 - 2 files changed, 5 deletions(-) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index a30d39a6..9f99dda7 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,7 +7,6 @@ #include "generated_plugin_registrant.h" #include -#include #include #include @@ -15,9 +14,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); - g_autoptr(FlPluginRegistrar) rive_common_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "RivePlugin"); - rive_plugin_register_with_registrar(rive_common_registrar); g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index bec16405..74369f25 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux - rive_common sqlite3_flutter_libs url_launcher_linux ) From 721dd5e2983a99d186a392c8b23311f51a671a4e Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 14:58:06 +0200 Subject: [PATCH 04/20] fix async gap --- lib/widgets/nutrition/widgets.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index 2da1593d..9843aa53 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -173,6 +173,9 @@ class _IngredientTypeaheadState extends State { if (widget.barcode!.isNotEmpty) { final result = await Provider.of(context, listen: false) .searchIngredientWithCode(widget.barcode!); + if (!mounted) { + return; + } if (result != null) { showDialog( From abd531be1f55cb8decbd3bd9bf45a4cb218b8a62 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 15:14:04 +0200 Subject: [PATCH 05/20] bugfix: show error message when scan fails due to the extra Navigator.pop(), we would immediately close the error dialog after opening it, rendering it practically invisible. to handle back button in non-error cases, we now simply accept the null value --- lib/widgets/nutrition/widgets.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index 9843aa53..68be4f9a 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -75,10 +75,13 @@ class _IngredientTypeaheadState extends State { var _searchEnglish = true; Future readerscan(BuildContext context) async { - String scannedcode; + String? scannedcode; try { scannedcode = await Navigator.of(context).push(MaterialPageRoute(builder: (context) => ScanReader())); + if (scannedcode == null) { + return ''; + } if (scannedcode.compareTo('-1') == 0) { return ''; @@ -164,6 +167,7 @@ class _IngredientTypeaheadState extends State { Widget scanButton() { return IconButton( key: const Key('scan-button'), + icon: const FaIcon(FontAwesomeIcons.barcode), onPressed: () async { try { if (!widget.test!) { @@ -228,15 +232,11 @@ class _IngredientTypeaheadState extends State { } } } catch (e) { - if (context.mounted) { + if (mounted) { showErrorDialog(e, context); - // Need to pop back since reader scan is a widget - // otherwise returns null when back button is pressed - return Navigator.pop(context); } } }, - icon: const FaIcon(FontAwesomeIcons.barcode), ); } } From cb696cc585ae6593dff5a44c14f754bfda9b2203 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 15:36:27 +0200 Subject: [PATCH 06/20] fix non-final field on immutable class --- lib/widgets/nutrition/widgets.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index 68be4f9a..f5a5a82b 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -35,14 +35,11 @@ import 'package:wger/widgets/core/core.dart'; import 'package:wger/widgets/nutrition/helpers.dart'; class ScanReader extends StatelessWidget { - String? scannedr; - @override Widget build(BuildContext context) => Scaffold( body: ReaderWidget( onScan: (result) { - scannedr = result.text; - Navigator.pop(context, scannedr); + Navigator.pop(context, result.text); }, ), ); From d896fab4ad73f52d1c63bb9283d9e260ab833def Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 15:50:26 +0200 Subject: [PATCH 07/20] cleanup --- lib/widgets/nutrition/widgets.dart | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index f5a5a82b..ddd490c0 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -39,6 +39,13 @@ class ScanReader extends StatelessWidget { Widget build(BuildContext context) => Scaffold( body: ReaderWidget( onScan: (result) { + // notes: + // 1. even if result.isValid, result.error is always non-null (and set to "") + // 2. i've never encountered scan errors to see when they occur, and + // i wouldn't know what to do about them anyway, so we simply return + // result.text in such case (which presumably will be null, or "") + // 3. when user cancels (swipe left / back button) this code is no longer + // run and the caller receives null Navigator.pop(context, result.text); }, ), @@ -72,22 +79,20 @@ class _IngredientTypeaheadState extends State { var _searchEnglish = true; Future readerscan(BuildContext context) async { - String? scannedcode; try { - scannedcode = - await Navigator.of(context).push(MaterialPageRoute(builder: (context) => ScanReader())); - if (scannedcode == null) { + final code = await Navigator.of(context) + .push(MaterialPageRoute(builder: (context) => ScanReader())); + if (code == null) { return ''; } - if (scannedcode.compareTo('-1') == 0) { + if (code.compareTo('-1') == 0) { return ''; } + return code; } on PlatformException { return ''; } - - return scannedcode; } @override From c8dbb8971e6566940b9acf224ea8805d441c37ca Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 15:54:43 +0200 Subject: [PATCH 08/20] fix various lint issues --- lib/widgets/nutrition/widgets.dart | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index ddd490c0..c307bb0e 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -56,14 +56,11 @@ class IngredientTypeahead extends StatefulWidget { final TextEditingController _ingredientController; final TextEditingController _ingredientIdController; - String? barcode = ''; - - //Code? result; - - late final bool? test; + final String barcode; + final bool? test; final bool showScanner; - IngredientTypeahead( + const IngredientTypeahead( this._ingredientIdController, this._ingredientController, { this.showScanner = true, @@ -77,6 +74,13 @@ class IngredientTypeahead extends StatefulWidget { class _IngredientTypeaheadState extends State { var _searchEnglish = true; + late String barcode; + + @override + void initState() { + super.initState(); + barcode = widget.barcode; // for unit tests + } Future readerscan(BuildContext context) async { try { @@ -173,12 +177,15 @@ class _IngredientTypeaheadState extends State { onPressed: () async { try { if (!widget.test!) { - widget.barcode = await readerscan(context); + barcode = await readerscan(context); } - if (widget.barcode!.isNotEmpty) { + if (barcode.isNotEmpty) { + if (!mounted) { + return; + } final result = await Provider.of(context, listen: false) - .searchIngredientWithCode(widget.barcode!); + .searchIngredientWithCode(barcode); if (!mounted) { return; } @@ -218,7 +225,7 @@ class _IngredientTypeaheadState extends State { key: const Key('notFound-dialog'), title: Text(AppLocalizations.of(context).productNotFound), content: Text( - AppLocalizations.of(context).productNotFoundDescription(widget.barcode!), + AppLocalizations.of(context).productNotFoundDescription(barcode), ), actions: [ TextButton( From 5ae30e67883799d1f53a4382b5ac3df96b6e17a7 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Sun, 19 May 2024 08:52:20 +0200 Subject: [PATCH 09/20] cleaner way to show line of macros --- lib/widgets/dashboard/widgets.dart | 19 ++----------------- lib/widgets/nutrition/helpers.dart | 9 +++++++++ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/lib/widgets/dashboard/widgets.dart b/lib/widgets/dashboard/widgets.dart index 93297d4a..4d37b167 100644 --- a/lib/widgets/dashboard/widgets.dart +++ b/lib/widgets/dashboard/widgets.dart @@ -42,6 +42,7 @@ import 'package:wger/widgets/measurements/charts.dart'; import 'package:wger/widgets/measurements/forms.dart'; import 'package:wger/widgets/nutrition/charts.dart'; import 'package:wger/widgets/nutrition/forms.dart'; +import 'package:wger/widgets/nutrition/helpers.dart'; import 'package:wger/widgets/weight/forms.dart'; import 'package:wger/widgets/workouts/forms.dart'; @@ -83,23 +84,7 @@ class _DashboardNutritionWidgetState extends State { //textAlign: TextAlign.left, ), ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.min, - children: [ - MutedText( - '${AppLocalizations.of(context).energyShort} ${meal.plannedNutritionalValues.energy.toStringAsFixed(0)}${AppLocalizations.of(context).kcal}'), - const MutedText(' / '), - MutedText( - '${AppLocalizations.of(context).proteinShort} ${meal.plannedNutritionalValues.protein.toStringAsFixed(0)}${AppLocalizations.of(context).g}'), - const MutedText(' / '), - MutedText( - '${AppLocalizations.of(context).carbohydratesShort} ${meal.plannedNutritionalValues.carbohydrates.toStringAsFixed(0)}${AppLocalizations.of(context).g}'), - const MutedText(' / '), - MutedText( - '${AppLocalizations.of(context).fatShort} ${meal.plannedNutritionalValues.fat.toStringAsFixed(0)}${AppLocalizations.of(context).g} '), - ], - ), + MutedText(getShortNutritionValues(meal.plannedNutritionalValues, context)), IconButton( icon: const Icon(Icons.history_edu), color: wgerPrimaryButtonColor, diff --git a/lib/widgets/nutrition/helpers.dart b/lib/widgets/nutrition/helpers.dart index 222e8765..766386b1 100644 --- a/lib/widgets/nutrition/helpers.dart +++ b/lib/widgets/nutrition/helpers.dart @@ -40,3 +40,12 @@ List getMutedNutritionalValues(NutritionalValues values, BuildContext co textAlign: TextAlign.right, ), ]; + +String getShortNutritionValues(NutritionalValues values, BuildContext context) { + final loc = AppLocalizations.of(context); + final e = '${loc.energyShort} ${loc.kcalValue(values.energy.toStringAsFixed(0))}'; + final p = '${loc.proteinShort} ${loc.gValue(values.protein.toStringAsFixed(0))}'; + final c = '${loc.carbohydratesShort} ${loc.gValue(values.carbohydrates.toStringAsFixed(0))}'; + final f = '${loc.fatShort} ${loc.gValue(values.fat.toStringAsFixed(0))}'; + return '$e / $p / $c / $f'; +} From ad4543f2e671205f8f6dc3642b0b1e12d92d70b2 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Sun, 19 May 2024 09:12:59 +0200 Subject: [PATCH 10/20] small refactor --- lib/models/nutrition/ingredient.dart | 14 ++++++++++++++ lib/models/nutrition/log.dart | 13 +------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/models/nutrition/ingredient.dart b/lib/models/nutrition/ingredient.dart index 59a3460f..87684c46 100644 --- a/lib/models/nutrition/ingredient.dart +++ b/lib/models/nutrition/ingredient.dart @@ -18,6 +18,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/models/nutrition/image.dart'; +import 'package:wger/models/nutrition/nutritional_values.dart'; part 'ingredient.g.dart'; @@ -91,4 +92,17 @@ class Ingredient { factory Ingredient.fromJson(Map json) => _$IngredientFromJson(json); Map toJson() => _$IngredientToJson(this); + + NutritionalValues get nutritionalValues { + return NutritionalValues.values( + energy * 1, + protein * 1, + carbohydrates * 1, + carbohydratesSugar * 1, + fat * 1, + fatSaturated * 1, + fibres * 1, + sodium * 1, + ); + } } diff --git a/lib/models/nutrition/log.dart b/lib/models/nutrition/log.dart index 730763cc..ac4aa845 100644 --- a/lib/models/nutrition/log.dart +++ b/lib/models/nutrition/log.dart @@ -83,21 +83,10 @@ class Log { /// Calculations NutritionalValues get nutritionalValues { // This is already done on the server. It might be better to read it from there. - final out = NutritionalValues(); - //final weight = amount; final weight = weightUnitObj == null ? amount : amount * weightUnitObj!.amount * weightUnitObj!.grams; - out.energy = ingredient.energy * weight / 100; - out.protein = ingredient.protein * weight / 100; - out.carbohydrates = ingredient.carbohydrates * weight / 100; - out.carbohydratesSugar = ingredient.carbohydratesSugar * weight / 100; - out.fat = ingredient.fat * weight / 100; - out.fatSaturated = ingredient.fatSaturated * weight / 100; - out.fibres = ingredient.fibres * weight / 100; - out.sodium = ingredient.sodium * weight / 100; - - return out; + return ingredient.nutritionalValues / (100 / weight); } } From 63d37175c4377f75b3564e15936a2b12ff69c385 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Sun, 19 May 2024 09:35:07 +0200 Subject: [PATCH 11/20] show reminder of macro's on ingredient tiles --- lib/widgets/nutrition/forms.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 6bf3fcb0..2280380c 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -28,6 +28,7 @@ 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/helpers.dart'; import 'package:wger/widgets/nutrition/widgets.dart'; class MealForm extends StatelessWidget { @@ -210,8 +211,10 @@ class MealItemForm extends StatelessWidget { _mealItem.ingredientId = _listMealItems[index].ingredientId; _mealItem.amount = _listMealItems[index].amount; }, - title: Text(_listMealItems[index].ingredient.name), - subtitle: Text('${_listMealItems[index].amount.toStringAsFixed(0)}$unit'), + title: Text( + '${_listMealItems[index].ingredient.name} (${_listMealItems[index].amount.toStringAsFixed(0)}$unit)'), + subtitle: Text(getShortNutritionValues( + _listMealItems[index].ingredient.nutritionalValues, context)), trailing: const Icon(Icons.copy), ), ); @@ -376,8 +379,10 @@ class IngredientLogForm extends StatelessWidget { _mealItem.ingredientId = diaryEntries[index].ingredientId; _mealItem.amount = diaryEntries[index].amount; }, - title: Text(_plan.diaryEntries[index].ingredient.name), - subtitle: Text('${diaryEntries[index].amount.toStringAsFixed(0)}$unit'), + title: Text( + '${diaryEntries[index].ingredient.name} (${diaryEntries[index].amount.toStringAsFixed(0)}$unit)'), + subtitle: Text(getShortNutritionValues( + diaryEntries[index].ingredient.nutritionalValues, context)), trailing: const Icon(Icons.copy), ), ); From 1c84d918348135cb48777c75c40f5ceeef38afa5 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 09:24:07 +0200 Subject: [PATCH 12/20] convert form to stateful, show ingredient preview --- lib/widgets/nutrition/forms.dart | 109 ++++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 25 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 2280380c..a3dc37ca 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -23,6 +23,7 @@ import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/helpers/ui.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'; @@ -228,19 +229,27 @@ class MealItemForm extends StatelessWidget { } } -class IngredientLogForm extends StatelessWidget { - late MealItem _mealItem; +class IngredientLogForm extends StatefulWidget { final NutritionalPlan _plan; + const IngredientLogForm(this._plan); + @override + State createState() => _IngredientLogFormState(); +} + +class _IngredientLogFormState extends State { final _form = GlobalKey(); final _ingredientController = TextEditingController(); final _ingredientIdController = TextEditingController(); final _amountController = TextEditingController(); final _dateController = TextEditingController(); final _timeController = TextEditingController(); + final _mealItem = MealItem.empty(); - IngredientLogForm(this._plan) { - _mealItem = MealItem.empty(); + bool validIngredientId = false; + @override + void initState() { + super.initState(); final now = DateTime.now(); _dateController.text = toDate(now)!; _timeController.text = timeToString(TimeOfDay.fromDateTime(now))!; @@ -248,7 +257,7 @@ class IngredientLogForm extends StatelessWidget { @override Widget build(BuildContext context) { - final diaryEntries = _plan.diaryEntries; + final diaryEntries = widget._plan.diaryEntries; final String unit = AppLocalizations.of(context).g; return Container( @@ -261,25 +270,32 @@ class IngredientLogForm extends StatelessWidget { _ingredientIdController, _ingredientController, ), - TextFormField( - decoration: InputDecoration(labelText: AppLocalizations.of(context).weight), - controller: _amountController, - keyboardType: TextInputType.number, - onFieldSubmitted: (_) {}, - onSaved: (newValue) { - _mealItem.amount = double.parse(newValue!); - }, - validator: (value) { - try { - double.parse(value!); - } catch (error) { - return AppLocalizations.of(context).enterValidNumber; - } - return null; - }, - ), Row( children: [ + Expanded( + child: TextFormField( + decoration: InputDecoration(labelText: AppLocalizations.of(context).weight), + controller: _amountController, + keyboardType: TextInputType.number, + onFieldSubmitted: (_) {}, + onChanged: (value) { + setState(() { + _mealItem.amount = double.tryParse(value) ?? _mealItem.amount; + }); + }, + onSaved: (value) { + _mealItem.amount = double.parse(value!); + }, + validator: (value) { + try { + double.parse(value!); + } catch (error) { + return AppLocalizations.of(context).enterValidNumber; + } + return null; + }, + ), + ), Expanded( child: TextFormField( readOnly: true, @@ -337,6 +353,46 @@ class IngredientLogForm extends StatelessWidget { ), ], ), + if (validIngredientId) + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Text( + 'Macros preview', + style: Theme.of(context).textTheme.titleMedium, + ), + FutureBuilder( + future: Provider.of(context, listen: false) + .fetchIngredient(_mealItem.ingredientId), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + _mealItem.ingredient = snapshot.data!; + return Padding( + padding: const EdgeInsets.only(top: 16), + child: + Text(getShortNutritionValues(_mealItem.nutritionalValues, context)), + ); + } else if (snapshot.hasError) { + return Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + 'Ingredient lookup error: ${snapshot.error}', + style: const TextStyle(color: Colors.red), + ), + ); + } else { + return const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(), + ); + } + }, + ), + ], + ), + ), ElevatedButton( child: Text(AppLocalizations.of(context).save), onPressed: () async { @@ -351,7 +407,7 @@ class IngredientLogForm extends StatelessWidget { final tod = stringToTime(_timeController.text); date = DateTime(date.year, date.month, date.day, tod.hour, tod.minute); Provider.of(context, listen: false) - .logIngredientToDiary(_mealItem, _plan.id!, date); + .logIngredientToDiary(_mealItem, widget._plan.id!, date); } on WgerHttpException catch (error) { showHttpExceptionErrorDialog(error, context); } catch (error) { @@ -376,8 +432,11 @@ class IngredientLogForm extends StatelessWidget { _ingredientController.text = diaryEntries[index].ingredient.name; _ingredientIdController.text = diaryEntries[index].ingredient.id.toString(); _amountController.text = diaryEntries[index].amount.toStringAsFixed(0); - _mealItem.ingredientId = diaryEntries[index].ingredientId; - _mealItem.amount = diaryEntries[index].amount; + setState(() { + _mealItem.ingredientId = diaryEntries[index].ingredientId; + _mealItem.amount = diaryEntries[index].amount; + validIngredientId = true; + }); }, title: Text( '${diaryEntries[index].ingredient.name} (${diaryEntries[index].amount.toStringAsFixed(0)}$unit)'), From 58122d40b5373a2a7c6c72a48f28178901531b4b Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 11:06:14 +0200 Subject: [PATCH 13/20] get ready to support showing images in the ingredient preview note: the image doesn't seem to be set yet, but at least the UI is now ready for it --- lib/widgets/nutrition/forms.dart | 6 +++--- lib/widgets/nutrition/meal.dart | 28 ++-------------------------- lib/widgets/nutrition/widgets.dart | 22 ++++++++++++++++++++++ 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index a3dc37ca..763d9dfd 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -368,9 +368,9 @@ class _IngredientLogFormState extends State { builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { _mealItem.ingredient = snapshot.data!; - return Padding( - padding: const EdgeInsets.only(top: 16), - child: + return ListTile( + leading: IngredientAvatar(ingredient: _mealItem.ingredient), + title: Text(getShortNutritionValues(_mealItem.nutritionalValues, context)), ); } else if (snapshot.hasError) { diff --git a/lib/widgets/nutrition/meal.dart b/lib/widgets/nutrition/meal.dart index 3048bed0..253fde07 100644 --- a/lib/widgets/nutrition/meal.dart +++ b/lib/widgets/nutrition/meal.dart @@ -20,13 +20,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:wger/helpers/consts.dart'; -import 'package:wger/helpers/misc.dart'; import 'package:wger/models/nutrition/log.dart'; import 'package:wger/models/nutrition/meal.dart'; import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/providers/nutrition.dart'; import 'package:wger/screens/form_screen.dart'; -import 'package:wger/widgets/core/core.dart'; import 'package:wger/widgets/nutrition/charts.dart'; import 'package:wger/widgets/nutrition/forms.dart'; import 'package:wger/widgets/nutrition/helpers.dart'; @@ -208,18 +206,7 @@ class MealItemWidget extends StatelessWidget { final values = _item.nutritionalValues; return ListTile( - leading: _item.ingredient.image != null - ? GestureDetector( - child: CircleAvatar(backgroundImage: NetworkImage(_item.ingredient.image!.image)), - onTap: () async { - if (_item.ingredient.image!.objectUrl != '') { - return launchURL(_item.ingredient.image!.objectUrl, context); - } else { - return; - } - }, - ) - : const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)), + leading: IngredientAvatar(ingredient: _item.ingredient), title: Text( '${_item.amount.toStringAsFixed(0)}$unit ${_item.ingredient.name}', overflow: TextOverflow.ellipsis, @@ -273,18 +260,7 @@ class LogDiaryItemWidget extends StatelessWidget { final values = _item.nutritionalValues; return ListTile( - leading: _item.ingredient.image != null - ? GestureDetector( - child: CircleAvatar(backgroundImage: NetworkImage(_item.ingredient.image!.image)), - onTap: () async { - if (_item.ingredient.image!.objectUrl != '') { - return launchURL(_item.ingredient.image!.objectUrl, context); - } else { - return; - } - }, - ) - : const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)), + leading: IngredientAvatar(ingredient: _item.ingredient), title: Text( '${_item.amount.toStringAsFixed(0)}$unit ${_item.ingredient.name}', overflow: TextOverflow.ellipsis, diff --git a/lib/widgets/nutrition/widgets.dart b/lib/widgets/nutrition/widgets.dart index c307bb0e..0cc2d24c 100644 --- a/lib/widgets/nutrition/widgets.dart +++ b/lib/widgets/nutrition/widgets.dart @@ -25,9 +25,11 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:wger/helpers/consts.dart'; +import 'package:wger/helpers/misc.dart'; import 'package:wger/helpers/platform.dart'; import 'package:wger/helpers/ui.dart'; import 'package:wger/models/exercises/ingredient_api.dart'; +import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/models/nutrition/log.dart'; import 'package:wger/models/nutrition/nutritional_plan.dart'; import 'package:wger/providers/nutrition.dart'; @@ -335,3 +337,23 @@ class NutritionDiaryEntry extends StatelessWidget { ); } } + +class IngredientAvatar extends StatelessWidget { + final Ingredient ingredient; + + const IngredientAvatar({super.key, required this.ingredient}); + + @override + Widget build(BuildContext context) { + return ingredient.image != null + ? GestureDetector( + child: CircleAvatar(backgroundImage: NetworkImage(ingredient.image!.image)), + onTap: () async { + if (ingredient.image!.objectUrl != '') { + return launchURL(ingredient.image!.objectUrl, context); + } + }, + ) + : const CircleIconAvatar(Icon(Icons.image, color: Colors.grey)); + } +} From 74002350ba036d445cbb46127304133e1d5b9dbb Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 13:16:27 +0200 Subject: [PATCH 14/20] unify IngredientLogForm and MealItemForm --- lib/widgets/nutrition/forms.dart | 304 +++++++----------- .../nutritional_meal_item_form_test.dart | 6 +- 2 files changed, 122 insertions(+), 188 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 763d9dfd..5d4b21d5 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -24,6 +24,7 @@ import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/helpers/ui.dart'; import 'package:wger/models/nutrition/ingredient.dart'; +import 'package:wger/models/nutrition/log.dart'; import 'package:wger/models/nutrition/meal.dart'; import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_plan.dart'; @@ -115,135 +116,57 @@ class MealForm extends StatelessWidget { } } -class MealItemForm extends StatelessWidget { - final Meal _meal; - late final MealItem _mealItem; - final List _listMealItems; - late String _barcode; - late bool _test; - - final _form = GlobalKey(); - final _ingredientIdController = TextEditingController(); - final _ingredientController = TextEditingController(); - final _amountController = TextEditingController(); - - MealItemForm(this._meal, this._listMealItems, [mealItem, code, test]) { - _mealItem = mealItem ?? MealItem.empty(); - _test = test ?? false; - _barcode = code ?? ''; - _mealItem.mealId = _meal.id!; - } - - TextEditingController get ingredientIdController => _ingredientIdController; - - MealItem get mealItem => _mealItem; - - @override - Widget build(BuildContext context) { - final String unit = AppLocalizations.of(context).g; - return Container( - margin: const EdgeInsets.all(20), - child: Form( - key: _form, - child: Column( - children: [ - IngredientTypeahead( - _ingredientIdController, - _ingredientController, - barcode: _barcode, - test: _test, - ), - TextFormField( - key: const Key('field-weight'), - decoration: InputDecoration(labelText: AppLocalizations.of(context).weight), - controller: _amountController, - keyboardType: TextInputType.number, - onFieldSubmitted: (_) {}, - onSaved: (newValue) { - _mealItem.amount = double.parse(newValue!); - }, - validator: (value) { - try { - double.parse(value!); - } catch (error) { - return AppLocalizations.of(context).enterValidNumber; - } - return null; - }, - ), - ElevatedButton( - key: const Key(SUBMIT_BUTTON_KEY_NAME), - child: Text(AppLocalizations.of(context).save), - onPressed: () async { - if (!_form.currentState!.validate()) { - return; - } - _form.currentState!.save(); - _mealItem.ingredientId = int.parse(_ingredientIdController.text); - - try { - Provider.of(context, listen: false) - .addMealItem(_mealItem, _meal); - } on WgerHttpException catch (error) { - showHttpExceptionErrorDialog(error, context); - } catch (error) { - showErrorDialog(error, context); - } - Navigator.of(context).pop(); - }, - ), - if (_listMealItems.isNotEmpty) const SizedBox(height: 10.0), - Container( - padding: const EdgeInsets.all(10.0), - child: Text(AppLocalizations.of(context).recentlyUsedIngredients), - ), - Expanded( - child: ListView.builder( - itemCount: _listMealItems.length, - shrinkWrap: true, - itemBuilder: (context, index) { - return Card( - child: ListTile( - onTap: () { - _ingredientController.text = _listMealItems[index].ingredient.name; - _ingredientIdController.text = - _listMealItems[index].ingredient.id.toString(); - _amountController.text = _listMealItems[index].amount.toStringAsFixed(0); - _mealItem.ingredientId = _listMealItems[index].ingredientId; - _mealItem.amount = _listMealItems[index].amount; - }, - title: Text( - '${_listMealItems[index].ingredient.name} (${_listMealItems[index].amount.toStringAsFixed(0)}$unit)'), - subtitle: Text(getShortNutritionValues( - _listMealItems[index].ingredient.nutritionalValues, context)), - trailing: const Icon(Icons.copy), - ), - ); - }, - ), - ) - ], - ), - ), - ); - } +Widget MealItemForm(Meal meal, List recent, [String? barcode, bool? test]) { + return IngredientForm( + // TODO we use planId 0 here cause we don't have one and we don't need it I think? + recent: recent.map((e) => Log.fromMealItem(e, 0, e.mealId)).toList(), + onSave: (BuildContext context, MealItem mealItem, DateTime? dt) { + mealItem.mealId = meal.id!; + Provider.of(context, listen: false).addMealItem(mealItem, meal); + }, + barcode: barcode ?? '', + test: test ?? false, + withDate: false); } -class IngredientLogForm extends StatefulWidget { - final NutritionalPlan _plan; - const IngredientLogForm(this._plan); - - @override - State createState() => _IngredientLogFormState(); +Widget IngredientLogForm(NutritionalPlan plan) { + return IngredientForm( + recent: plan.diaryEntries, + onSave: (BuildContext context, MealItem mealItem, DateTime? dt) { + Provider.of(context, listen: false) + .logIngredientToDiary(mealItem, plan.id!, dt); + }, + withDate: true); } -class _IngredientLogFormState extends State { +/// IngredientForm is a form that lets the user pick an ingredient (and amount) to +/// log to the diary or to add to a meal. +class IngredientForm extends StatefulWidget { + final Function(BuildContext context, MealItem mealItem, DateTime? dt) onSave; + final List recent; + final bool withDate; + final String barcode; + final bool test; + + const IngredientForm({ + required this.recent, + required this.onSave, + required this.withDate, + this.barcode = '', + this.test = false, + }); + + @override + State createState() => _IngredientFormState(); +} + +class _IngredientFormState extends State { final _form = GlobalKey(); final _ingredientController = TextEditingController(); final _ingredientIdController = TextEditingController(); final _amountController = TextEditingController(); - final _dateController = TextEditingController(); - final _timeController = TextEditingController(); + final _dateController = TextEditingController(); // optional + final _timeController = TextEditingController(); // optional final _mealItem = MealItem.empty(); bool validIngredientId = false; @@ -255,9 +178,12 @@ class _IngredientLogFormState extends State { _timeController.text = timeToString(TimeOfDay.fromDateTime(now))!; } + TextEditingController get ingredientIdController => _ingredientIdController; + + MealItem get mealItem => _mealItem; + @override Widget build(BuildContext context) { - final diaryEntries = widget._plan.diaryEntries; final String unit = AppLocalizations.of(context).g; return Container( @@ -269,11 +195,14 @@ class _IngredientLogFormState extends State { IngredientTypeahead( _ingredientIdController, _ingredientController, + barcode: widget.barcode, + test: widget.test, ), Row( children: [ Expanded( child: TextFormField( + key: const Key('field-weight'), // needed ? decoration: InputDecoration(labelText: AppLocalizations.of(context).weight), controller: _amountController, keyboardType: TextInputType.number, @@ -296,61 +225,63 @@ class _IngredientLogFormState extends State { }, ), ), - Expanded( - child: TextFormField( - readOnly: true, - // Stop keyboard from appearing - decoration: InputDecoration( - labelText: AppLocalizations.of(context).date, - // suffixIcon: const Icon(Icons.calendar_today), - ), - enableInteractiveSelection: false, - controller: _dateController, - onTap: () async { - // Show Date Picker Here - final pickedDate = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(DateTime.now().year - 10), - lastDate: DateTime.now(), - ); - - if (pickedDate != null) { - _dateController.text = toDate(pickedDate)!; - } - }, - onSaved: (newValue) { - _dateController.text = newValue!; - }, - ), - ), - Expanded( - child: TextFormField( - key: const Key('field-time'), - decoration: InputDecoration( - labelText: AppLocalizations.of(context).time, - //suffixIcon: const Icon(Icons.punch_clock) - ), - controller: _timeController, - onTap: () async { + if (widget.withDate) + Expanded( + child: TextFormField( + readOnly: true, // Stop keyboard from appearing - FocusScope.of(context).requestFocus(FocusNode()); + decoration: InputDecoration( + labelText: AppLocalizations.of(context).date, + // suffixIcon: const Icon(Icons.calendar_today), + ), + enableInteractiveSelection: false, + controller: _dateController, + onTap: () async { + // Show Date Picker Here + final pickedDate = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(DateTime.now().year - 10), + lastDate: DateTime.now(), + ); - // Open time picker - final pickedTime = await showTimePicker( - context: context, - initialTime: stringToTime(_timeController.text), - ); - if (pickedTime != null) { - _timeController.text = timeToString(pickedTime)!; - } - }, - onSaved: (newValue) { - _timeController.text = newValue!; - }, - onFieldSubmitted: (_) {}, + if (pickedDate != null) { + _dateController.text = toDate(pickedDate)!; + } + }, + onSaved: (newValue) { + _dateController.text = newValue!; + }, + ), + ), + if (widget.withDate) + Expanded( + child: TextFormField( + key: const Key('field-time'), + decoration: InputDecoration( + labelText: AppLocalizations.of(context).time, + //suffixIcon: const Icon(Icons.punch_clock) + ), + controller: _timeController, + onTap: () async { + // Stop keyboard from appearing + FocusScope.of(context).requestFocus(FocusNode()); + + // Open time picker + final pickedTime = await showTimePicker( + context: context, + initialTime: stringToTime(_timeController.text), + ); + if (pickedTime != null) { + _timeController.text = timeToString(pickedTime)!; + } + }, + onSaved: (newValue) { + _timeController.text = newValue!; + }, + onFieldSubmitted: (_) {}, + ), ), - ), ], ), if (validIngredientId) @@ -394,6 +325,9 @@ class _IngredientLogFormState extends State { ), ), ElevatedButton( + key: const Key( + SUBMIT_BUTTON_KEY_NAME), // needed? mealItemForm had it, but not ingredientlogform + child: Text(AppLocalizations.of(context).save), onPressed: () async { if (!_form.currentState!.validate()) { @@ -406,8 +340,7 @@ class _IngredientLogFormState extends State { var date = DateTime.parse(_dateController.text); final tod = stringToTime(_timeController.text); date = DateTime(date.year, date.month, date.day, tod.hour, tod.minute); - Provider.of(context, listen: false) - .logIngredientToDiary(_mealItem, widget._plan.id!, date); + widget.onSave(context, _mealItem, date); } on WgerHttpException catch (error) { showHttpExceptionErrorDialog(error, context); } catch (error) { @@ -416,32 +349,33 @@ class _IngredientLogFormState extends State { Navigator.of(context).pop(); }, ), - if (diaryEntries.isNotEmpty) const SizedBox(height: 10.0), + if (widget.recent.isNotEmpty) const SizedBox(height: 10.0), Container( padding: const EdgeInsets.all(10.0), child: Text(AppLocalizations.of(context).recentlyUsedIngredients), ), Expanded( child: ListView.builder( - itemCount: diaryEntries.length, + itemCount: widget.recent.length, shrinkWrap: true, itemBuilder: (context, index) { return Card( child: ListTile( onTap: () { - _ingredientController.text = diaryEntries[index].ingredient.name; - _ingredientIdController.text = diaryEntries[index].ingredient.id.toString(); - _amountController.text = diaryEntries[index].amount.toStringAsFixed(0); + _ingredientController.text = widget.recent[index].ingredient.name; + _ingredientIdController.text = + widget.recent[index].ingredient.id.toString(); + _amountController.text = widget.recent[index].amount.toStringAsFixed(0); setState(() { - _mealItem.ingredientId = diaryEntries[index].ingredientId; - _mealItem.amount = diaryEntries[index].amount; + _mealItem.ingredientId = widget.recent[index].ingredientId; + _mealItem.amount = widget.recent[index].amount; validIngredientId = true; }); }, title: Text( - '${diaryEntries[index].ingredient.name} (${diaryEntries[index].amount.toStringAsFixed(0)}$unit)'), + '${widget.recent[index].ingredient.name} (${widget.recent[index].amount.toStringAsFixed(0)}$unit)'), subtitle: Text(getShortNutritionValues( - diaryEntries[index].ingredient.nutritionalValues, context)), + widget.recent[index].ingredient.nutritionalValues, context)), trailing: const Icon(Icons.copy), ), ); diff --git a/test/nutrition/nutritional_meal_item_form_test.dart b/test/nutrition/nutritional_meal_item_form_test.dart index 50561722..2a751a6d 100644 --- a/test/nutrition/nutritional_meal_item_form_test.dart +++ b/test/nutrition/nutritional_meal_item_form_test.dart @@ -101,7 +101,7 @@ void main() { home: Scaffold( body: Scrollable( viewportBuilder: (BuildContext context, ViewportOffset position) => - MealItemForm(meal, const [], null, code, test), + MealItemForm(meal, const [], code, test), ), ), routes: { @@ -213,7 +213,7 @@ void main() { testWidgets('confirm found ingredient dialog', (WidgetTester tester) async { await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true)); - final MealItemForm formScreen = tester.widget(find.byType(MealItemForm)); + final IngredientForm formScreen = tester.widget(find.byType(IngredientForm)); await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); @@ -293,7 +293,7 @@ void main() { (WidgetTester tester) async { await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true)); - final MealItemForm formScreen = tester.widget(find.byType(MealItemForm)); + final IngredientForm formScreen = tester.widget(find.byType(IngredientForm)); await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); From d4d298ad9e9f639097f26e71b6773de7a0acd493 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 14:13:00 +0200 Subject: [PATCH 15/20] fix tests --- lib/widgets/nutrition/forms.dart | 9 ++++++--- test/nutrition/nutritional_meal_item_form_test.dart | 10 +++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index 5d4b21d5..f5b2e7c1 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -157,10 +157,10 @@ class IngredientForm extends StatefulWidget { }); @override - State createState() => _IngredientFormState(); + State createState() => IngredientFormState(); } -class _IngredientFormState extends State { +class IngredientFormState extends State { final _form = GlobalKey(); final _ingredientController = TextEditingController(); final _ingredientIdController = TextEditingController(); @@ -209,7 +209,10 @@ class _IngredientFormState extends State { onFieldSubmitted: (_) {}, onChanged: (value) { setState(() { - _mealItem.amount = double.tryParse(value) ?? _mealItem.amount; + final v = double.tryParse(value); + if (v != null) { + _mealItem.amount = v; + } }); }, onSaved: (value) { diff --git a/test/nutrition/nutritional_meal_item_form_test.dart b/test/nutrition/nutritional_meal_item_form_test.dart index 2a751a6d..aa739ba1 100644 --- a/test/nutrition/nutritional_meal_item_form_test.dart +++ b/test/nutrition/nutritional_meal_item_form_test.dart @@ -213,7 +213,7 @@ void main() { testWidgets('confirm found ingredient dialog', (WidgetTester tester) async { await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true)); - final IngredientForm formScreen = tester.widget(find.byType(IngredientForm)); + final IngredientFormState formState = tester.state(find.byType(IngredientForm)); await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); @@ -223,7 +223,7 @@ void main() { await tester.tap(find.byKey(const Key('found-dialog-confirm-button'))); await tester.pumpAndSettle(); - expect(formScreen.ingredientIdController.text, '1'); + expect(formState.ingredientIdController.text, '1'); }); testWidgets('close found ingredient dialog', (WidgetTester tester) async { @@ -293,7 +293,7 @@ void main() { (WidgetTester tester) async { await tester.pumpWidget(createMealItemFormScreen(meal1, '123', true)); - final IngredientForm formScreen = tester.widget(find.byType(IngredientForm)); + final IngredientFormState formState = tester.state(find.byType(IngredientForm)); await tester.tap(find.byKey(const Key('scan-button'))); await tester.pumpAndSettle(); @@ -303,7 +303,7 @@ void main() { await tester.tap(find.byKey(const Key('found-dialog-confirm-button'))); await tester.pumpAndSettle(); - expect(formScreen.ingredientIdController.text, '1'); + expect(formState.ingredientIdController.text, '1'); await tester.enterText(find.byKey(const Key('field-weight')), '2'); await tester.pumpAndSettle(); @@ -313,7 +313,7 @@ void main() { await tester.tap(find.byKey(const Key(SUBMIT_BUTTON_KEY_NAME))); await tester.pumpAndSettle(); - expect(formScreen.mealItem.amount, 2); + expect(formState.mealItem.amount, 2); verify(mockNutrition.addMealItem(any, meal1)); }); From eeec221499152e8c74f1e431a06c80d8cbfef85b Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Tue, 21 May 2024 14:22:10 +0200 Subject: [PATCH 16/20] add note --- lib/models/nutrition/meal_item.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/models/nutrition/meal_item.dart b/lib/models/nutrition/meal_item.dart index d484283f..4bf10e94 100644 --- a/lib/models/nutrition/meal_item.dart +++ b/lib/models/nutrition/meal_item.dart @@ -72,6 +72,7 @@ class MealItem { Map toJson() => _$MealItemToJson(this); /// Calculations + /// TODO why does this not consider weightUnitObj ? should we do the same as Log.nutritionalValues here? NutritionalValues get nutritionalValues { // This is already done on the server. It might be better to read it from there. final out = NutritionalValues(); From 98953de9855d9774f837bfa7fc6ead5c83f042f5 Mon Sep 17 00:00:00 2001 From: Tsz Hong CHAN Date: Fri, 24 May 2024 06:20:08 +0000 Subject: [PATCH 17/20] Translated using Weblate (Chinese (Simplified)) Currently translated at 64.4% (167 of 259 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/zh_Hans/ --- lib/l10n/app_zh.arb | 54 +++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index e7c4b276..95b3564b 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -61,7 +61,7 @@ "@workoutSession": { "description": "A (logged) workout session" }, - "sameRepetitions": "填写次数和量级,则自动计算出相乘的结果作为数量", + "sameRepetitions": "如果您对所有组别进行相同的重复次数和重量,则只需填写一行即可。例如,对于 4 组,只需输入 10 次重复,这会自动变成「4 x 10」。", "@sameRepetitions": {}, "exercise": "锻炼项目", "@exercise": { @@ -87,7 +87,7 @@ "@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" }, @@ -113,7 +113,7 @@ "@newWorkout": { "description": "Header when adding a new workout" }, - "category": "目录", + "category": "类别", "@category": { "description": "Category for an exercise, ingredient, etc." }, @@ -157,7 +157,7 @@ "@labelWorkoutPlans": { "description": "Title for screen workout plans" }, - "loginInstead": "登录", + "loginInstead": "已经有帐号了吗?立即登入", "@loginInstead": {}, "registerInstead": "没有帐户?点击注册", "@registerInstead": {}, @@ -228,7 +228,7 @@ "@aboutSourceTitle": { "description": "Title for source code section in the about dialog" }, - "aboutDescription": "谢谢使用!", + "aboutDescription": "感谢您使用wger! wger 是一个协作开源项目,由来自世界各地的健身爱好者创建。", "@aboutDescription": { "description": "Text in the about dialog" }, @@ -348,13 +348,13 @@ "@addMeal": {}, "save": "保存", "@save": {}, - "name": "给你的计划起个名字", + "name": "名", "@name": { "description": "Name for a workout or nutritional plan" }, - "description": "详细信息", + "description": "描述", "@description": {}, - "logHelpEntriesUnits": "有计量单位和数量的会以图形化的方式展现,类似时间、未完成的项目将不会出现在这里", + "logHelpEntriesUnits": "请注意,系统只会绘制带重量单位(公斤或磅)和重复次数的纪录,其他组合(例如时间或直到力竭)将被忽略。", "@logHelpEntriesUnits": {}, "selectIngredient": "请选择一个营养成分", "@selectIngredient": { @@ -385,17 +385,17 @@ "@addSet": { "description": "Label for the button that adds a set (to a workout day)" }, - "logHelpEntries": "同一项目在当天有多次记录的情况下, 图中只显示训练量较大的项目", + "logHelpEntries": "如果一天内有多个相同重复次数但重量不同的条目,图表只会显示重量较高的条目。", "@logHelpEntries": {}, "plateCalculator": "圈", "@plateCalculator": { "description": "Label used for the plate calculator in the gym mode" }, - "rirNotUsed": "未设置组内重复次数", + "rirNotUsed": "保留次数未储存", "@rirNotUsed": { "description": "Label used in RiR slider when the RiR value is not used/saved for the current setting or log" }, - "rir": "组内重复次数", + "rir": "保留次数", "@rir": { "description": "Shorthand for Repetitions In Reserve" }, @@ -443,7 +443,7 @@ "@aboutContactUsText": { "description": "Text for contact us section in the about dialog" }, - "aboutBugsText": "反馈一下你的使用体验吧", + "aboutBugsText": "如果某些情况未如预期运作或您认为缺少某个功能,请与我们联络。", "@aboutBugsText": { "description": "Text for bugs section in the about dialog" }, @@ -467,7 +467,7 @@ "@weekAverage": { "description": "Header for the column of '7 day average' nutritional values, i.e. what was logged last week" }, - "productFoundDescription": "对应此产品的条形码为{productName}。是否继续?{productName}", + "productFoundDescription": "此条码对应这产品:{productName}。您想继续吗?", "@productFoundDescription": { "description": "Dialog info when product is found with barcode", "type": "text", @@ -487,7 +487,7 @@ "@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" }, @@ -537,9 +537,9 @@ "@measurements": { "description": "Categories for the measurements such as biceps size, body fat, etc." }, - "measurementCategoriesHelpText": "测量类别,如“上臂围”或“体脂”", + "measurementCategoriesHelpText": "测量类别,例如“二头肌”或“体脂”", "@measurementCategoriesHelpText": {}, - "measurementEntriesHelpText": "用于度量的单位,如\"cm\"或\"%\"。", + "measurementEntriesHelpText": "用于测量类别的单位,例如「cm」或「%」", "@measurementEntriesHelpText": {}, "value": "数值", "@value": { @@ -562,5 +562,25 @@ "exerciseName": "锻炼名", "@exerciseName": { "description": "Label for the name of a workout exercise" - } + }, + "previous": "前一个", + "@previous": {}, + "kg": "公斤", + "@kg": { + "description": "Generated entry for translation for server strings" + }, + "verify": "确认", + "@verify": {}, + "next": "下一个", + "@next": {}, + "success": "成功", + "@success": { + "description": "Message when an action completed successfully, usually used as a heading" + }, + "lb": "磅", + "@lb": { + "description": "Generated entry for translation for server strings" + }, + "alternativeNames": "別称", + "@alternativeNames": {} } From bc56e38ac4ca97d54df73f345c26c0d81e28da66 Mon Sep 17 00:00:00 2001 From: Tsz Hong CHAN Date: Thu, 23 May 2024 14:02:55 +0000 Subject: [PATCH 18/20] Translated using Weblate (Japanese) Currently translated at 66.7% (173 of 259 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/ja/ --- lib/l10n/app_ja.arb | 62 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 19ad4374..b35b8877 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -143,11 +143,11 @@ "@reps": { "description": "Shorthand for repetitions, used when space constraints are tighter" }, - "rir": "反復準備", + "rir": "予備レップ", "@rir": { "description": "Shorthand for Repetitions In Reserve" }, - "rirNotUsed": "反復準備が使用されていません", + "rirNotUsed": "予備レップは保存されていません", "@rirNotUsed": { "description": "Label used in RiR slider when the RiR value is not used/saved for the current setting or log" }, @@ -558,5 +558,61 @@ "description": "Translation for close" }, "userProfile": "あなたのプロフィール", - "@userProfile": {} + "@userProfile": {}, + "barbell": "バーベル", + "@barbell": { + "description": "Generated entry for translation for server strings" + }, + "arms": "腕", + "@arms": { + "description": "Generated entry for translation for server strings" + }, + "chest": "胸", + "@chest": { + "description": "Generated entry for translation for server strings" + }, + "dumbbell": "ダンベル", + "@dumbbell": { + "description": "Generated entry for translation for server strings" + }, + "success": "成功", + "@success": { + "description": "Message when an action completed successfully, usually used as a heading" + }, + "cardio": "有酸素運動", + "@cardio": { + "description": "Generated entry for translation for server strings" + }, + "bench": "ベンチ", + "@bench": { + "description": "Generated entry for translation for server strings" + }, + "settingsCacheDescription": "エクササイズキャッシュ", + "@settingsCacheDescription": {}, + "settingsTitle": "設定", + "@settingsTitle": {}, + "settingsCacheTitle": "キャッシュ", + "@settingsCacheTitle": {}, + "back": "戻る", + "@back": { + "description": "Generated entry for translation for server strings" + }, + "abs": "腹筋", + "@abs": { + "description": "Generated entry for translation for server strings" + }, + "biceps": "力こぶ", + "@biceps": { + "description": "Generated entry for translation for server strings" + }, + "body_weight": "体重", + "@body_weight": { + "description": "Generated entry for translation for server strings" + }, + "verify": "確認する", + "@verify": {}, + "calves": "ふくらはぎ", + "@calves": { + "description": "Generated entry for translation for server strings" + } } From 1133c5c418cfe8dfa00e8adbc5ffa292d4cc3b4f Mon Sep 17 00:00:00 2001 From: Tsz Hong CHAN Date: Thu, 23 May 2024 08:30:40 +0000 Subject: [PATCH 19/20] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (259 of 259 strings) Translation: wger Workout Manager/Mobile App Translate-URL: https://hosted.weblate.org/projects/wger/mobile/zh_Hant/ --- lib/l10n/app_zh_Hant.arb | 763 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 761 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index e197cc7a..a571c2c8 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -83,7 +83,7 @@ "@exercise": { "description": "An exercise for a workout" }, - "exercises": "動作", + "exercises": "訓練", "@exercises": { "description": "Multiple exercises for a workout" }, @@ -136,5 +136,764 @@ "description": "Equipment needed to perform an exercise" }, "searchNamesInEnglish": "也用英文字詞搜尋", - "@searchNamesInEnglish": {} + "@searchNamesInEnglish": {}, + "logged": "紀錄", + "@logged": { + "description": "Header for the column of 'logged' nutritional values, i.e. what was eaten" + }, + "scanBarcode": "掃描條形碼", + "@scanBarcode": { + "description": "Label for scan barcode button" + }, + "seconds": "秒", + "@seconds": { + "description": "Generated entry for translation for server strings" + }, + "useMetric": "用公制單位表示體重", + "@useMetric": {}, + "set": "組", + "@set": { + "description": "A set in a workout plan" + }, + "repetitionUnit": "重複次數單位", + "@repetitionUnit": {}, + "dayDescriptionHelp": "描述這一天做了什麼(例如:拉力日)或訓練了哪些身體部位(例如:胸部和肩膀)", + "@dayDescriptionHelp": {}, + "comment": "註釋", + "@comment": { + "description": "Comment, additional information" + }, + "impression": "感想", + "@impression": { + "description": "General impression (e.g. for a workout session) such as good, bad, etc." + }, + "sameRepetitions": "如果您對所有組別進行相同的重複次數和重量,則只需填寫一行即可。例如,對於 4 組,只需輸入 10 次重複,這會自動變成「4 x 10」。", + "@sameRepetitions": {}, + "plateCalculator": "槓片", + "@plateCalculator": { + "description": "Label used for the plate calculator in the gym mode" + }, + "gymMode": "健身模式", + "@gymMode": { + "description": "Label when starting the gym mode" + }, + "selectExercises": "如果你想做一個超級組,你可以搜尋幾個訓練,它們會組在一起。", + "@selectExercises": {}, + "workoutSession": "訓練(節)", + "@workoutSession": { + "description": "A (logged) workout session" + }, + "pause": "暫停", + "@pause": { + "description": "Noun, not an imperative! Label used for the pause when using the gym mode" + }, + "jumpTo": "跳到", + "@jumpTo": { + "description": "Imperative. Label used in popup allowing the user to jump to a specific exercise while in the gym mode" + }, + "todaysWorkout": "您今天的訓練", + "@todaysWorkout": {}, + "logHelpEntries": "如果一天內有多個相同重複次數但重量不同的條目,圖表只會顯示重量較高的條目。", + "@logHelpEntries": {}, + "description": "描述", + "@description": {}, + "addIngredient": "增加材料", + "@addIngredient": {}, + "verify": "確認", + "@verify": {}, + "logMeal": "紀錄這餐", + "@logMeal": {}, + "logHelpEntriesUnits": "請注意,系統只會繪製帶重量單位(公斤或磅)和重複次數的紀錄,其他組合(例如時間或直到力竭)將被忽略。", + "@logHelpEntriesUnits": {}, + "name": "名", + "@name": { + "description": "Name for a workout or nutritional plan" + }, + "nutritionalDiary": "營養日記", + "@nutritionalDiary": {}, + "nutritionalPlans": "營養計劃", + "@nutritionalPlans": {}, + "weight": "重量", + "@weight": { + "description": "The weight of a workout log or body weight entry" + }, + "time": "時間", + "@time": { + "description": "The time of a meal or workout" + }, + "searchIngredient": "搜尋材料", + "@searchIngredient": { + "description": "Label on ingredient search form" + }, + "logIngredient": "儲存到營養日記", + "@logIngredient": {}, + "anErrorOccurred": "發生錯誤!", + "@anErrorOccurred": {}, + "noNutritionalPlans": "你沒有營養計劃", + "@noNutritionalPlans": { + "description": "Message shown when the user has no nutritional plans" + }, + "timeStart": "開始時間", + "@timeStart": { + "description": "The starting time of a workout" + }, + "timeEnd": "完結時間", + "@timeEnd": { + "description": "The end time of a workout" + }, + "timeStartAhead": "開始時間不能早於結束時間", + "@timeStartAhead": {}, + "saturatedFat": "飽和脂肪", + "@saturatedFat": {}, + "fatShort": "脂肪", + "@fatShort": { + "description": "The first letter or short name of the word 'Fat', used in overviews" + }, + "fibres": "纖維", + "@fibres": {}, + "sodium": "鈉", + "@sodium": {}, + "edit": "編輯", + "@edit": {}, + "moreMeasurementEntries": "增加新的測量", + "@moreMeasurementEntries": { + "description": "Message shown when the user wants to add new measurement" + }, + "confirmDelete": "您確定要刪除'{toDelete}'嗎?", + "@confirmDelete": { + "description": "Confirmation text before the user deletes an object", + "type": "text", + "placeholders": { + "toDelete": {} + } + }, + "aboutBugsTitle": "有問題或想法嗎?", + "@aboutBugsTitle": { + "description": "Title for bugs section in the about dialog" + }, + "newNutritionalPlan": "新的營養計劃", + "@newNutritionalPlan": {}, + "aboutSourceText": "在 Github 上取得這應用程式的原始程式碼及伺服器", + "@aboutSourceText": { + "description": "Text for source code section in the about dialog" + }, + "aboutDescription": "感謝您使用wger! wger 是一個協作開源項目,由來自世界各地的健身愛好者創建。", + "@aboutDescription": { + "description": "Text in the about dialog" + }, + "enterValue": "請輸入數值", + "@enterValue": { + "description": "Error message when the user hasn't entered a value on a required field" + }, + "enterRepetitionsOrWeight": "請為至少一組輸入重複次數或重量", + "@enterRepetitionsOrWeight": { + "description": "Error message when the user hasn't filled in the forms for exercise sets" + }, + "aboutBugsText": "如果某些情況未如預期運作或您認為缺少某個功能,請與我們聯絡。", + "@aboutBugsText": { + "description": "Text for bugs section in the about dialog" + }, + "previous": "前一個", + "@previous": {}, + "add_exercise_image_license": "影像必須與 CC BY SA 授權相容。如有疑問,請僅上傳你自己拍攝的照片。", + "@add_exercise_image_license": {}, + "verifiedEmail": "已認證電郵", + "@verifiedEmail": {}, + "selectIngredient": "請選擇材料", + "@selectIngredient": { + "description": "Error message when the user hasn't selected an ingredient from the autocompleter" + }, + "barbell": "槓鈴", + "@barbell": { + "description": "Generated entry for translation for server strings" + }, + "arms": "手臂", + "@arms": { + "description": "Generated entry for translation for server strings" + }, + "contributeExerciseWarning": "只有當您的帳戶已超過 {days} 天並且已驗證您的電子郵件時,您才可以貢獻訓練", + "@contributeExerciseWarning": { + "description": "Number of days before which a person can add exercise", + "placeholders": { + "days": { + "type": "String", + "example": "14" + } + } + }, + "dumbbell": "啞鈴", + "@dumbbell": { + "description": "Generated entry for translation for server strings" + }, + "chest": "胸", + "@chest": { + "description": "Generated entry for translation for server strings" + }, + "calves": "小腿肌群", + "@calves": { + "description": "Generated entry for translation for server strings" + }, + "bench": "平板凳", + "@bench": { + "description": "Generated entry for translation for server strings" + }, + "lb": "磅", + "@lb": { + "description": "Generated entry for translation for server strings" + }, + "until_failure": "直到力竭", + "@until_failure": { + "description": "Generated entry for translation for server strings" + }, + "none__bodyweight_exercise_": "無(自重訓練)", + "@none__bodyweight_exercise_": { + "description": "Generated entry for translation for server strings" + }, + "done": "完成", + "@done": {}, + "log": "紀錄", + "@log": { + "description": "Log a specific meal (imperative form)" + }, + "addMeal": "増加餐單", + "@addMeal": {}, + "sugars": "糖", + "@sugars": {}, + "fat": "脂肪", + "@fat": {}, + "calendar": "日曆", + "@calendar": {}, + "optionsLabel": "選項", + "@optionsLabel": { + "description": "Label for the popup with general app options" + }, + "weightUnit": "重量單位", + "@weightUnit": {}, + "newDay": "新一天", + "@newDay": {}, + "newSet": "新組", + "@newSet": { + "description": "Header when adding a new set to a workout day" + }, + "start": "開始", + "@start": { + "description": "Label on button to start the gym mode (i.e., an imperative)" + }, + "selectExercise": "請選擇一個訓練", + "@selectExercise": { + "description": "Error message when the user hasn't selected an exercise in the form" + }, + "whatVariationsExist": "此練習有哪些變體(如果有)?", + "@whatVariationsExist": {}, + "back": "背部", + "@back": { + "description": "Generated entry for translation for server strings" + }, + "category": "類別", + "@category": { + "description": "Category for an exercise, ingredient, etc." + }, + "newWorkout": "新健身計畫", + "@newWorkout": { + "description": "Header when adding a new workout" + }, + "musclesSecondary": "輔助肌", + "@musclesSecondary": { + "description": "secondary muscles trained by an exercise" + }, + "rirNotUsed": "保留次數未儲存", + "@rirNotUsed": { + "description": "Label used in RiR slider when the RiR value is not used/saved for the current setting or log" + }, + "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": {} + } + }, + "notes": "筆記", + "@notes": { + "description": "Personal notes, e.g. for a workout session" + }, + "plateCalculatorNotDivisible": "沒有足夠的槓片達到目標重量", + "@plateCalculatorNotDivisible": { + "description": "Error message when the current weight is not reachable with plates (e.g. 33.1 kg)" + }, + "save": "儲存", + "@save": {}, + "addSet": "加組", + "@addSet": { + "description": "Label for the button that adds a set (to a workout day)" + }, + "mealLogged": "膳食已記錄到日記", + "@mealLogged": {}, + "measurement": "測量", + "@measurement": {}, + "nutritionalPlan": "營養計劃", + "@nutritionalPlan": {}, + "date": "日期", + "@date": { + "description": "The date of a workout log or body weight entry" + }, + "value": "數值", + "@value": { + "description": "The value of a measurement entry" + }, + "measurementEntriesHelpText": "用於測量類別的單位,例如「cm」或「%」", + "@measurementEntriesHelpText": {}, + "measurements": "測量", + "@measurements": { + "description": "Categories for the measurements such as biceps size, body fat, etc." + }, + "planned": "計劃", + "@planned": { + "description": "Header for the column of 'planned' nutritional values, i.e. what should be eaten" + }, + "gPerBodyKg": "克/體重公斤", + "@gPerBodyKg": { + "description": "Label used for total sums of e.g. calories or similar in grams per Kg of body weight" + }, + "kJ": "千焦", + "@kJ": { + "description": "Energy in a meal in kilo joules, kJ" + }, + "carbohydratesShort": "碳水化合物", + "@carbohydratesShort": { + "description": "The first letter or short name of the word 'Carbohydrates', used in overviews" + }, + "unit": "單位", + "@unit": { + "description": "The unit used for a repetition (kg, time, etc.)" + }, + "newEntry": "新條目", + "@newEntry": { + "description": "Title when adding a new entry such as a weight or log entry" + }, + "amount": "分量", + "@amount": { + "description": "The amount (e.g. in grams) of an ingredient in a meal" + }, + "loadingText": "載入中...", + "@loadingText": { + "description": "Text to show when entries are being loaded in the background: Loading..." + }, + "delete": "刪除", + "@delete": {}, + "aboutSourceTitle": "原始碼", + "@aboutSourceTitle": { + "description": "Title for source code section in the about dialog" + }, + "goToDetailPage": "進入詳情頁面", + "@goToDetailPage": {}, + "aboutContactUsText": "如果您想與我們聊天,請到 Discord 伺服器並取得聯繫", + "@aboutContactUsText": { + "description": "Text for contact us section in the about dialog" + }, + "aboutMastodonText": "在 Mastodon 上關注我們,以了解有關這項目的更新和新聞", + "@aboutMastodonText": { + "description": "Text for the mastodon section in the about dialog" + }, + "appUpdateTitle": "需要更新", + "@appUpdateTitle": {}, + "variations": "變體", + "@variations": { + "description": "Variations of one exercise (e.g. benchpress and benchpress narrow)" + }, + "legs": "腿", + "@legs": { + "description": "Generated entry for translation for server strings" + }, + "lower_back": "下背", + "@lower_back": { + "description": "Generated entry for translation for server strings" + }, + "max_reps": "最大次數", + "@max_reps": { + "description": "Generated entry for translation for server strings" + }, + "quads": "股四頭肌", + "@quads": { + "description": "Generated entry for translation for server strings" + }, + "textPromptTitle": "準備開始?", + "@textPromptTitle": {}, + "body_weight": "體重", + "@body_weight": { + "description": "Generated entry for translation for server strings" + }, + "textPromptSubheading": "按操作按鈕開始", + "@textPromptSubheading": {}, + "ingredient": "材料", + "@ingredient": {}, + "energy": "能量", + "@energy": { + "description": "Energy in a meal, ingredient etc. e.g. in kJ" + }, + "goalEnergy": "能量目標", + "@goalEnergy": {}, + "goalProtein": "蛋白質目標", + "@goalProtein": {}, + "goalFat": "脂肪目標", + "@goalFat": {}, + "onlyLogging": "只追蹤卡路里", + "@onlyLogging": {}, + "addGoalsToPlan": "新增目標至此計劃", + "@addGoalsToPlan": {}, + "measurementCategoriesHelpText": "測量類別,例如“二頭肌”或“體脂”", + "@measurementCategoriesHelpText": {}, + "today": "今天", + "@today": {}, + "percentEnergy": "能量百分比", + "@percentEnergy": {}, + "onlyLoggingHelpText": "如果你只想記錄卡路里但不想設計有特定膳食的詳細營養計劃,請選中此框。", + "@onlyLoggingHelpText": {}, + "addGoalsToPlanHelpText": "這使你可以為計劃設定能量、蛋白質、碳水化合物或脂肪的總體目標。請注意,如果你設定了詳細的膳食計劃,這些數值將取得優先權。", + "@addGoalsToPlanHelpText": {}, + "goalCarbohydrates": "碳水化合物目標", + "@goalCarbohydrates": {}, + "loggedToday": "在今天紀錄", + "@loggedToday": {}, + "verifiedEmailReason": "您需要驗證您的電子郵件才能貢獻訓練項目", + "@verifiedEmailReason": {}, + "alternativeNames": "別稱", + "@alternativeNames": {}, + "productNotFound": "找不到產品", + "@productNotFound": { + "description": "Header label for dialog when product is not found with barcode" + }, + "productNotFoundDescription": "在 wger 資料庫中找不到帶有掃描條碼 {barcode} 的產品", + "@productNotFoundDescription": { + "description": "Dialog info when product is not found with barcode", + "type": "text", + "placeholders": { + "barcode": {} + } + }, + "images": "圖片", + "@images": {}, + "language": "語言", + "@language": {}, + "translateExercise": "立即翻譯此訓練", + "@translateExercise": {}, + "translation": "翻譯", + "@translation": {}, + "contributeExercise": "貢獻一個訓練", + "@contributeExercise": {}, + "baseData": "基礎數據(英文)", + "@baseData": { + "description": "The base data for an exercise such as category, trained muscles, etc." + }, + "cacheWarning": "由於緩存中,改變可能需要一些時間才能在應用程式顯示。", + "@cacheWarning": {}, + "miles": "英里", + "@miles": { + "description": "Generated entry for translation for server strings" + }, + "kilometers_per_hour": "公里/小時", + "@kilometers_per_hour": { + "description": "Generated entry for translation for server strings" + }, + "kilometers": "公里", + "@kilometers": { + "description": "Generated entry for translation for server strings" + }, + "kettlebell": "壺鈴", + "@kettlebell": { + "description": "Generated entry for translation for server strings" + }, + "lats": "背闊肌", + "@lats": { + "description": "Generated entry for translation for server strings" + }, + "kg": "公斤", + "@kg": { + "description": "Generated entry for translation for server strings" + }, + "triceps": "三頭肌", + "@triceps": { + "description": "Generated entry for translation for server strings" + }, + "shoulders": "肩膀", + "@shoulders": { + "description": "Generated entry for translation for server strings" + }, + "unVerifiedEmail": "未認證電郵", + "@unVerifiedEmail": {}, + "glutes": "臀大肌", + "@glutes": { + "description": "Generated entry for translation for server strings" + }, + "cardio": "帶氧運動", + "@cardio": { + "description": "Generated entry for translation for server strings" + }, + "miles_per_hour": "英里/小時", + "@miles_per_hour": { + "description": "Generated entry for translation for server strings" + }, + "supersetWith": "超級組", + "@supersetWith": { + "description": "Text used between exercise cards when adding a new set. Translate as something like 'in a superset with'" + }, + "weekAverage": "七天平均值", + "@weekAverage": { + "description": "Header for the column of '7 day average' nutritional values, i.e. what was logged last week" + }, + "difference": "差異", + "@difference": {}, + "productFound": "找到產品", + "@productFound": { + "description": "Header label for dialog when product is found with barcode" + }, + "close": "關閉", + "@close": { + "description": "Translation for close" + }, + "biceps": "二頭肌", + "@biceps": { + "description": "Generated entry for translation for server strings" + }, + "oneNamePerLine": "每行一個名稱", + "@oneNamePerLine": {}, + "aboutPageTitle": "關於Wger", + "@aboutPageTitle": {}, + "verifiedEmailInfo": "驗證電郵已發送至 {email}", + "@verifiedEmailInfo": { + "placeholders": { + "email": {} + } + }, + "gym_mat": "健身墊", + "@gym_mat": { + "description": "Generated entry for translation for server strings" + }, + "settingsCacheDeletedSnackbar": "成功清除快取", + "@settingsCacheDeletedSnackbar": {}, + "settingsTitle": "設定", + "@settingsTitle": {}, + "settingsCacheTitle": "緩存", + "@settingsCacheTitle": {}, + "settingsCacheDescription": "訓練緩存", + "@settingsCacheDescription": {}, + "gValue": "{value} 克", + "@gValue": { + "description": "A value in grams, e.g. 5 g", + "type": "text", + "placeholders": { + "value": {} + } + }, + "percentValue": "{value} %", + "@percentValue": { + "description": "A value in percent, e.g. 10 %", + "type": "text", + "placeholders": { + "value": {} + } + }, + "noWorkoutPlans": "你沒有健身計畫", + "@noWorkoutPlans": { + "description": "Message shown when the user has no workout plans" + }, + "reps": "次", + "@reps": { + "description": "Shorthand for repetitions, used when space constraints are tighter" + }, + "rir": "保留次數", + "@rir": { + "description": "Shorthand for Repetitions In Reserve" + }, + "selectEntry": "請選擇一個條目", + "@selectEntry": {}, + "nrOfSets": "組 / 訓練: {nrOfSets}", + "@nrOfSets": { + "description": "Label shown on the slider where the user selects the nr of sets", + "type": "text", + "placeholders": { + "nrOfSets": {} + } + }, + "alsoKnownAs": "又稱: {aliases}", + "@alsoKnownAs": { + "placeholders": { + "aliases": {} + }, + "description": "List of alternative names for an exercise" + }, + "abs": "腹肌", + "@abs": { + "description": "Generated entry for translation for server strings" + }, + "energyShort": "能量", + "@energyShort": { + "description": "The first letter or short name of the word 'Energy', used in overviews" + }, + "kcal": "千卡", + "@kcal": { + "description": "Energy in a meal in kilocalories, kcal" + }, + "macronutrients": "宏量營養素", + "@macronutrients": {}, + "proteinShort": "蛋白質", + "@proteinShort": { + "description": "The first letter or short name of the word 'Protein', used in overviews" + }, + "toggleDetails": "切換詳情", + "@toggleDetails": { + "description": "Switch to toggle detail / overview" + }, + "aboutContactUsTitle": "您好!", + "@aboutContactUsTitle": { + "description": "Title for contact us section in the about dialog" + }, + "addImage": "增加圖片", + "@addImage": {}, + "addExercise": "增加鍛鍊項目", + "@addExercise": {}, + "productFoundDescription": "此條碼對應這產品:{productName}。您想繼續嗎?", + "@productFoundDescription": { + "description": "Dialog info when product is found with barcode", + "type": "text", + "placeholders": { + "productName": {} + } + }, + "appUpdateContent": "此版本的應用程式與伺服器不相容,請更新您的應用程式。", + "@appUpdateContent": {}, + "aboutTranslationText": "這應用程式是在 weblate 上翻譯的。如果您也想提供協助,請點擊連結並開始翻譯", + "@aboutTranslationText": { + "description": "Text for translation section in the about dialog" + }, + "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": {} + } + }, + "g": "克", + "@g": { + "description": "Abbreviation for gram" + }, + "protein": "蛋白質", + "@protein": {}, + "carbohydrates": "碳水化合物", + "@carbohydrates": {}, + "surplus": "過剩", + "@surplus": { + "description": "Caloric surplus (either planned or unplanned)" + }, + "deficit": "赤字", + "@deficit": { + "description": "Caloric deficit (either planned or unplanned)" + }, + "kcalValue": "{value} 千卡", + "@kcalValue": { + "description": "A value in kcal, e.g. 500 kcal", + "type": "text", + "placeholders": { + "value": {} + } + }, + "noWeightEntries": "您沒有體重條目", + "@noWeightEntries": { + "description": "Message shown when the user has no logged weight entries" + }, + "noMeasurementEntries": "您沒有測量條目", + "@noMeasurementEntries": {}, + "enterMinCharacters": "請輸入至少 {min} 個字符", + "@enterMinCharacters": { + "description": "Error message when the user hasn't entered the minimum amount characters in a form", + "type": "text", + "placeholders": { + "min": {} + } + }, + "baseNameEnglish": "所有訓練都需要一個英文基本名稱", + "@baseNameEnglish": {}, + "aboutDonateText": "請我們喝杯咖啡來幫助項目,支付服務器費用,並讓我們保持經費", + "@aboutDonateText": {}, + "aboutMastodonTitle": "Mastodon", + "@aboutMastodonTitle": { + "description": "Title for mastodon section in the about dialog" + }, + "aboutTranslationTitle": "翻譯", + "@aboutTranslationTitle": { + "description": "Title for translation section in the about dialog" + }, + "aboutDonateTitle": "捐贈", + "@aboutDonateTitle": {}, + "goToToday": "轉到今天", + "@goToToday": { + "description": "Label on button to jump back to 'today' in the calendar widget" + }, + "total": "總共", + "@total": { + "description": "Label used for total sums of e.g. calories or similar" + }, + "swiss_ball": "抗力球", + "@swiss_ball": { + "description": "Generated entry for translation for server strings" + }, + "sz_bar": "彎曲槓", + "@sz_bar": { + "description": "Generated entry for translation for server strings" + }, + "pull_up_bar": "引體上升桿", + "@pull_up_bar": { + "description": "Generated entry for translation for server strings" + }, + "plates": "槓片", + "@plates": { + "description": "Generated entry for translation for server strings" + }, + "minutes": "分鐘", + "@minutes": { + "description": "Generated entry for translation for server strings" + }, + "incline_bench": "上斜凳", + "@incline_bench": { + "description": "Generated entry for translation for server strings" + }, + "hamstrings": "大腿後肌", + "@hamstrings": { + "description": "Generated entry for translation for server strings" + }, + "selectImage": "請選擇圖片", + "@selectImage": { + "description": "Label and error message when the user hasn't selected an image to save" + }, + "chooseFromLibrary": "從照片庫中選擇", + "@chooseFromLibrary": {}, + "enterValidNumber": "請輸入有效數目", + "@enterValidNumber": { + "description": "Error message when the user has submitted an invalid number (e.g. '3,.,.,.')" + }, + "dataCopied": "資料已複製到新條目", + "@dataCopied": { + "description": "Snackbar message to show on copying data to a new log entry" + }, + "setUnitsAndRir": "設定單位和次數", + "@setUnitsAndRir": { + "description": "Label shown on the slider where the user can toggle showing units and RiR", + "type": "text" + }, + "recentlyUsedIngredients": "最近加入的材料", + "@recentlyUsedIngredients": { + "description": "A message when a user adds a new ingredient to a meal." + }, + "takePicture": "拍張照片", + "@takePicture": {}, + "gallery": "照片庫", + "@gallery": {}, + "next": "下一個", + "@next": {}, + "repetitions": "次數", + "@repetitions": { + "description": "Generated entry for translation for server strings" + } } From e388e4ee536a37d48413efdb179cbbfc80cc62f7 Mon Sep 17 00:00:00 2001 From: Tsz Hong CHAN Date: Thu, 23 May 2024 07:38:07 +0000 Subject: [PATCH 20/20] Translated using Weblate (Chinese (Traditional)) Currently translated at 75.0% (3 of 4 strings) Translation: wger Workout Manager/Play Store Translate-URL: https://hosted.weblate.org/projects/wger/play-store/zh_Hant/ --- .../android/zh-TW/full_description.txt | 39 +++++++++++++++++++ .../android/zh-TW/short_description.txt | 1 + 2 files changed, 40 insertions(+) create mode 100644 fastlane/metadata/android/zh-TW/full_description.txt create mode 100644 fastlane/metadata/android/zh-TW/short_description.txt diff --git a/fastlane/metadata/android/zh-TW/full_description.txt b/fastlane/metadata/android/zh-TW/full_description.txt new file mode 100644 index 00000000..66013f08 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/full_description.txt @@ -0,0 +1,39 @@ +每一個健身愛好者 - 與您的健身教練 WGER 一起,讓你更健康! + +你是否已經找到了排名第一的健身應用程序,並且喜歡創建自己的運動習慣? 無論你是哪種運動野獸——我們都有一個共同點:我們喜歡追蹤我們的健康數據 + +因此,我們不會因為你仍然使用小筆記本來管理你的健身旅程而批評你,但歡迎來到 2024 年! + +我們為你開發了 100% 免費的數位健康和健身追蹤器應用程序,壓縮到最相關的功能,讓你的生活更輕鬆。開始吧,繼續訓練並慶祝你的進步! + +wger 是一個開源項目,關於: +* 你的身體 +* 你的鍛煉 +* 你的進展 +* 你的數據 + +你的身體: +無需在谷歌上搜尋你最喜歡的食物的成分——從 78000 多種產品中選擇你的日常膳食並查看營養價值。將餐點加入營養計劃中,並在日曆中概述你的飲食。 + +你的鍛鍊: +你知道什麼對你的身體最好。從 200 種不同的練習中選擇越來越多的練習來創建你自己的鍛鍊。然後,使用健身房模式引導你完成訓練,同時一鍵記錄你的體重。 + +你的進展: +永遠不要忘記你的目標。追蹤你的體重並保留你的統計數據。 + +你的數據: +wger 是你的個人化健身日記——但你擁有自己的數據。使用 REST API 來存取它並用它做驚人的事情。 + +請注意:這個免費的應用程式不是基於額外的資金,我們不要求你捐款。不僅如此,它還是一個不斷發展的社區計畫。因此,請隨時為新功能做好準備! + +#OpenSource – 這是什麼意思? + +開源意味著這個應用程式的整個原始碼和它與之通訊的伺服器都是免費的,任何人都可以使用: +* 你想在自己的伺服器上為你還是為你當地的健身房運行 wger?都可以! +* 你是否錯過了某個功能並想要實現它?現在開始! +* 你想檢查任何地方都沒有發送任何東西嗎?你可以! + +加入我們的社區,成為來自世界各地的體育愛好者和 IT 極客的一份子。我們一直致力於調整和優化根據我們的需求量身定制的應用程式。我們喜歡你的意見,因此請隨時加入並貢獻你的願望和想法! + +-> 在 https://github.com/wger-project 上找到原始碼 +-> 在我們的 Discord 伺服器上提問或打個招呼 https://discord.gg/rPWFv6W diff --git a/fastlane/metadata/android/zh-TW/short_description.txt b/fastlane/metadata/android/zh-TW/short_description.txt new file mode 100644 index 00000000..b66d2c04 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/short_description.txt @@ -0,0 +1 @@ +健身、營養和體重紀錄器