Merge branch 'master' into feature/nutrition-goals

This commit is contained in:
Roland Geider
2024-02-18 11:48:58 +01:00
21 changed files with 574 additions and 67 deletions

View File

@@ -26,7 +26,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.16.x
flutter-version: 3.19.x
- name: Flutter info
run: |

View File

@@ -19,7 +19,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.16.x'
flutter-version: '3.19.x'
- run: dart --version
- run: flutter --version

View File

@@ -20,7 +20,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.16.x'
flutter-version: '3.19.x'
- name: Get dependencies
run: flutter pub get

View File

@@ -26,6 +26,7 @@
- Hanaa Allohibi - <https://github.com/hn-n>
- Shey Alnasrawi - <https://github.com/Milksheyke>
- Costas Korai - <https://github.com/watcher6280>
- Bassam Mutairi - <https://github.com/mutairibassam>
## Translators

View File

@@ -43,7 +43,7 @@ Alternatively, you can use the test server (the db is reset every day):
Install Flutter and all its dependencies, and create a new virtual device:
<https://flutter.dev/docs/get-started/install>.
The app currently uses flutter 3.16
The app currently uses flutter 3.19
### 3
The application will complain about an API key not being set. You can just

View File

@@ -101,7 +101,7 @@ void showHttpExceptionErrorDialog(WgerHttpException exception, BuildContext cont
// This call serves no purpose The dialog above doesn't seem to show
// unless this dummy call is present
// showDialog(context: context, builder: (context) => Container());
showDialog(context: context, builder: (context) => Container());
}
dynamic showDeleteDialog(

View File

@@ -512,7 +512,7 @@
"@aboutBugsText": {
"description": "Text for bugs section in the about dialog"
},
"aboutContactUsTitle": "Pozdravujte!",
"aboutContactUsTitle": "Řekněte ahoj!",
"@aboutContactUsTitle": {
"description": "Title for contact us section in the about dialog"
},
@@ -596,7 +596,7 @@
"@images": {},
"language": "Jazyk",
"@language": {},
"aboutPageTitle": "O Wger",
"aboutPageTitle": "O aplikaci Wger",
"@aboutPageTitle": {},
"contributeExercise": "Přispějte cvik",
"@contributeExercise": {},

View File

@@ -482,5 +482,136 @@
"description": "Title for source code section in the about dialog"
},
"measurement": "Medição",
"@measurement": {}
"@measurement": {},
"nrOfSets": "Séries por exercício: {nrOfSets}",
"@nrOfSets": {
"description": "Label shown on the slider where the user selects the nr of sets",
"type": "text",
"placeholders": {
"nrOfSets": {}
}
},
"useMetric": "Use unidades métricas para peso corporal",
"@useMetric": {},
"aboutMastodonTitle": "Mastodon",
"@aboutMastodonTitle": {
"description": "Title for mastodon section in the about dialog"
},
"selectEntry": "Por favor, selecione uma entrada",
"@selectEntry": {},
"noMatchingExerciseFound": "Nenhum exercício correspondente encontrado",
"@noMatchingExerciseFound": {
"description": "Message returned if no exercises match the searched string"
},
"selectExercise": "Por favor, selecione um exercício",
"@selectExercise": {
"description": "Error message when the user hasn't selected an exercise in the form"
},
"enterValidNumber": "Por favor, insira um número válido",
"@enterValidNumber": {
"description": "Error message when the user has submitted an invalid number (e.g. '3,.,.,.')"
},
"baseNameEnglish": "Todos os exercícios necessitam de um nome base em inglês",
"@baseNameEnglish": {},
"aboutMastodonText": "Siga-nos no Mastodon para atualizações e novidades sobre o projeto",
"@aboutMastodonText": {
"description": "Text for the mastodon section in the about dialog"
},
"labelBottomNavWorkout": "Treino",
"@labelBottomNavWorkout": {
"description": "Label used in bottom navigation, use a short word"
},
"reps": "Reps",
"@reps": {
"description": "Shorthand for repetitions, used when space constraints are tighter"
},
"rir": "ReR",
"@rir": {
"description": "Shorthand for Repetitions In Reserve"
},
"setUnitsAndRir": "Unidades e RIR da série",
"@setUnitsAndRir": {
"description": "Label shown on the slider where the user can toggle showing units and RiR",
"type": "text"
},
"aboutDonateTitle": "Doações",
"@aboutDonateTitle": {},
"aboutDonateText": "Compre-nos um café para ajudar no projeto, pague pelos custos do servidor e nos mantenha abastecidos",
"@aboutDonateText": {},
"enterCharacters": "Por favor, utilize entre {min} e {max} caracteres",
"@enterCharacters": {
"description": "Error message when the user hasn't entered the correct number of characters in a form",
"type": "text",
"placeholders": {
"min": {},
"max": {}
}
},
"enterMinCharacters": "Por favor, utilize pelo menos {min} caracteres",
"@enterMinCharacters": {
"description": "Error message when the user hasn't entered the minimum amount characters in a form",
"type": "text",
"placeholders": {
"min": {}
}
},
"gallery": "Galeria",
"@gallery": {},
"alsoKnownAs": "Também conhecido como:{aliases}",
"@alsoKnownAs": {
"placeholders": {
"aliases": {}
},
"description": "List of alternative names for an exercise"
},
"recentlyUsedIngredients": "Ingredientes adicionados recentemente",
"@recentlyUsedIngredients": {
"description": "A message when a user adds a new ingredient to a meal."
},
"addImage": "Adicionar imagem",
"@addImage": {},
"scanBarcode": "Escaneie o código de barras",
"@scanBarcode": {
"description": "Label for scan barcode button"
},
"dataCopied": "Dados copiados para o novo registro",
"@dataCopied": {
"description": "Snackbar message to show on copying data to a new log entry"
},
"appUpdateContent": "Esta versão do aplicativo não é compatível com o servidor, por favor atualize o aplicativo.",
"@appUpdateContent": {},
"productFound": "Produto encontrado",
"@productFound": {
"description": "Header label for dialog when product is found with barcode"
},
"appUpdateTitle": "Atualização necessária",
"@appUpdateTitle": {},
"takePicture": "Tire uma foto",
"@takePicture": {},
"selectIngredient": "Por favor, selecione um ingrediente",
"@selectIngredient": {
"description": "Error message when the user hasn't selected an ingredient from the autocompleter"
},
"selectImage": "Por favor, selecione uma imagem",
"@selectImage": {
"description": "Label and error message when the user hasn't selected an image to save"
},
"optionsLabel": "Opções",
"@optionsLabel": {
"description": "Label for the popup with general app options"
},
"productNotFound": "Produto não encontrado",
"@productNotFound": {
"description": "Header label for dialog when product is not found with barcode"
},
"variations": "Variações",
"@variations": {
"description": "Variations of one exercise (e.g. benchpress and benchpress narrow)"
},
"verifiedEmail": "Email verificado",
"@verifiedEmail": {},
"chooseFromLibrary": "Escolher na galeria",
"@chooseFromLibrary": {},
"unVerifiedEmail": "Email não verificado",
"@unVerifiedEmail": {}
}

View File

@@ -19,7 +19,7 @@
"@set": {
"description": "A set in a workout plan"
},
"labelBottomNavWorkout": "Workout",
"labelBottomNavWorkout": "Antrenament",
"@labelBottomNavWorkout": {
"description": "Label used in bottom navigation, use a short word"
},
@@ -65,7 +65,7 @@
"@passwordTooShort": {
"description": "Error message when the user a password that is too short"
},
"repetitionUnit": "Unitate de repetiții",
"repetitionUnit": "Unitatea de repetiții",
"@repetitionUnit": {},
"weightUnit": "Unitate de greutate",
"@weightUnit": {},
@@ -204,5 +204,125 @@
"description": "A (logged) workout session"
},
"confirmPassword": "Confirmă parola",
"@confirmPassword": {}
"@confirmPassword": {},
"newDay": "Zi nouă",
"@newDay": {},
"selectExercises": "Dacă vrei să faci un superset poți căuta mai multe exerciții, acestea vor fi grupate împreună",
"@selectExercises": {},
"logHelpEntries": "Dacă într-o singură zi există mai multe înregistrări cu același număr de repetări, dar greutăți diferite, în diagramă este prezentată doar intrarea cu greutatea mai mare.",
"@logHelpEntries": {},
"description": "Descriere",
"@description": {},
"name": "Nume",
"@name": {
"description": "Name for a workout or nutritional plan"
},
"measurements": "Măsurători",
"@measurements": {
"description": "Categories for the measurements such as biceps size, body fat, etc."
},
"measurementEntriesHelpText": "Unitatea utilizată pentru măsurarea categoriei, cum ar fi „cm” sau „%”",
"@measurementEntriesHelpText": {},
"useMetric": "Utilizați unități metrice pentru greutatea corporală",
"@useMetric": {},
"plateCalculator": "Discuri",
"@plateCalculator": {
"description": "Label used for the plate calculator in the gym mode"
},
"jumpTo": "Sari la",
"@jumpTo": {
"description": "Imperative. Label used in popup allowing the user to jump to a specific exercise while in the gym mode"
},
"logHelpEntriesUnits": "Rețineți că numai intrările cu o unitate de greutate (kg sau lb) și repetări sunt grafice, alte combinații precum timpul sau până la eșec sunt ignorate aici.",
"@logHelpEntriesUnits": {},
"save": "Salvează",
"@save": {},
"verify": "Verifică",
"@verify": {},
"addIngredient": "Adaugă ingredient",
"@addIngredient": {},
"logIngredient": "Salvează în jurnalul de nutriție",
"@logIngredient": {},
"nutritionalDiary": "Jurnal de nutriție",
"@nutritionalDiary": {},
"nutritionalPlans": "Planuri nutriționale",
"@nutritionalPlans": {},
"weight": "Greutate",
"@weight": {
"description": "The weight of a workout log or body weight entry"
},
"timeStart": "Timpul de începere",
"@timeStart": {
"description": "The starting time of a workout"
},
"timeEnd": "Sfârșitul timpului",
"@timeEnd": {
"description": "The end time of a workout"
},
"noMatchingExerciseFound": "Nu s-au găsit exerciții potrivite",
"@noMatchingExerciseFound": {
"description": "Message returned if no exercises match the searched string"
},
"gymMode": "Modul sală",
"@gymMode": {
"description": "Label when starting the gym mode"
},
"plateCalculatorNotDivisible": "Nu se poate atinge greutatea cu plăcile disponibile",
"@plateCalculatorNotDivisible": {
"description": "Error message when the current weight is not reachable with plates (e.g. 33.1 kg)"
},
"pause": "Pauză",
"@pause": {
"description": "Noun, not an imperative! Label used for the pause when using the gym mode"
},
"todaysWorkout": "Antrenamentul tău de astăzi",
"@todaysWorkout": {},
"addSet": "Adaugă set",
"@addSet": {
"description": "Label for the button that adds a set (to a workout day)"
},
"addMeal": "Adaugă masa",
"@addMeal": {},
"mealLogged": "Masa înregistrată în jurnal",
"@mealLogged": {},
"logMeal": "Înregistrează această masă",
"@logMeal": {},
"searchIngredient": "Caută ingredient",
"@searchIngredient": {
"description": "Label on ingredient search form"
},
"nutritionalPlan": "Planul nutrițional",
"@nutritionalPlan": {},
"noNutritionalPlans": "Nu ai planuri nutriționale",
"@noNutritionalPlans": {
"description": "Message shown when the user has no nutritional plans"
},
"anErrorOccurred": "A aparut o eroare!",
"@anErrorOccurred": {},
"timeStartAhead": "Ora de început nu poate fi înaintea orei de încheiere",
"@timeStartAhead": {},
"newSet": "Set nou",
"@newSet": {
"description": "Header when adding a new set to a workout day"
},
"measurement": "Măsurare",
"@measurement": {},
"measurementCategoriesHelpText": "Categoria de măsurare, cum ar fi „biceps” sau „grăsimi corporale”",
"@measurementCategoriesHelpText": {},
"date": "Data",
"@date": {
"description": "The date of a workout log or body weight entry"
},
"value": "Valoare",
"@value": {
"description": "The value of a measurement entry"
},
"start": "Start",
"@start": {
"description": "Label on button to start the gym mode (i.e., an imperative)"
},
"time": "Timp",
"@time": {
"description": "The time of a meal or workout"
}
}

View File

@@ -27,7 +27,7 @@ class WeightEntry {
int? id;
@JsonKey(required: true, fromJson: stringToNum, toJson: numToString)
late num weight;
late num weight = 0;
@JsonKey(required: true, toJson: toDate)
late DateTime date;
@@ -44,7 +44,14 @@ class WeightEntry {
}
}
WeightEntry copyWith({int? id, int? weight, DateTime? date}) => WeightEntry(
id: id,
weight: weight ?? this.weight,
date: date ?? this.date,
);
// Boilerplate
factory WeightEntry.fromJson(Map<String, dynamic> json) => _$WeightEntryFromJson(json);
Map<String, dynamic> toJson() => _$WeightEntryToJson(this);
}

View File

@@ -43,9 +43,9 @@ class BodyWeightProvider with ChangeNotifier {
_entries = [];
}
/// Returns the latest (newest) weight entry or null if there are no entries
WeightEntry? getLastEntry() {
return _entries.isNotEmpty ? _entries.last : null;
/// Returns the latest (newest) weight entry or null if there are none
WeightEntry? getNewestEntry() {
return _entries.isNotEmpty ? _entries.first : null;
}
WeightEntry findById(int id) {

View File

@@ -30,6 +30,8 @@ class WeightScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final lastWeightEntry = context.read<BodyWeightProvider>().getNewestEntry();
return Scaffold(
appBar: EmptyAppBar(AppLocalizations.of(context).weight),
floatingActionButton: FloatingActionButton(
@@ -43,7 +45,7 @@ class WeightScreen extends StatelessWidget {
FormScreen.routeName,
arguments: FormScreenArguments(
AppLocalizations.of(context).newEntry,
WeightForm(),
WeightForm(lastWeightEntry?.copyWith(id: null)),
),
);
},

View File

@@ -243,12 +243,10 @@ class DashboardWeightWidget extends StatefulWidget {
}
class _DashboardWeightWidgetState extends State<DashboardWeightWidget> {
late BodyWeightProvider weightEntriesData;
@override
Widget build(BuildContext context) {
final profile = context.read<UserProvider>().profile;
weightEntriesData = Provider.of<BodyWeightProvider>(context, listen: false);
final weightProvider = context.read<BodyWeightProvider>();
return Consumer<BodyWeightProvider>(
builder: (context, workoutProvider, child) => Card(
@@ -267,13 +265,13 @@ class _DashboardWeightWidgetState extends State<DashboardWeightWidget> {
),
Column(
children: [
if (weightEntriesData.items.isNotEmpty)
if (weightProvider.items.isNotEmpty)
Column(
children: [
SizedBox(
height: 200,
child: MeasurementChartWidgetFl(
weightEntriesData.items
weightProvider.items
.map((e) => MeasurementChartEntry(e.weight, e.date))
.toList(),
unit: profile!.isMetric
@@ -296,7 +294,7 @@ class _DashboardWeightWidgetState extends State<DashboardWeightWidget> {
FormScreen.routeName,
arguments: FormScreenArguments(
AppLocalizations.of(context).newEntry,
WeightForm(),
WeightForm(weightProvider.getNewestEntry()?.copyWith(id: null)),
),
);
},

View File

@@ -42,7 +42,8 @@ class NutritionalPlanDetailWidget extends StatelessWidget {
Widget build(BuildContext context) {
final nutritionalValues = _nutritionalPlan.nutritionalValues;
final valuesPercentage = _nutritionalPlan.energyPercentage(nutritionalValues);
final lastWeightEntry = Provider.of<BodyWeightProvider>(context, listen: false).getLastEntry();
final lastWeightEntry =
Provider.of<BodyWeightProvider>(context, listen: false).getNewestEntry();
final valuesGperKg = lastWeightEntry != null
? _nutritionalPlan.gPerBodyKg(lastWeightEntry.weight, nutritionalValues)
: null;

View File

@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:wger/exceptions/http_exception.dart';
import 'package:wger/helpers/json.dart';
@@ -34,7 +35,7 @@ class WeightForm extends StatelessWidget {
WeightForm([WeightEntry? weightEntry]) {
_weightEntry = weightEntry ?? WeightEntry(date: DateTime.now());
weightController.text = _weightEntry.id == null ? '' : _weightEntry.weight.toString();
weightController.text = _weightEntry.weight == 0 ? '' : _weightEntry.weight.toString();
dateController.text = toDate(_weightEntry.date)!;
}
@@ -46,10 +47,15 @@ class WeightForm extends StatelessWidget {
children: [
// Weight date
TextFormField(
readOnly: true, // Stop keyboard from appearing
key: const Key('dateInput'),
readOnly: true,
// Stop keyboard from appearing
decoration: InputDecoration(
labelText: AppLocalizations.of(context).date,
suffixIcon: const Icon(Icons.calendar_today),
suffixIcon: const Icon(
Icons.calendar_today,
key: Key('calendarIcon'),
),
),
enableInteractiveSelection: false,
controller: dateController,
@@ -83,7 +89,60 @@ class WeightForm extends StatelessWidget {
// Weight
TextFormField(
decoration: InputDecoration(labelText: AppLocalizations.of(context).weight),
key: const Key('weightInput'),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).weight,
prefix: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
key: const Key('quickMinus'),
icon: const FaIcon(FontAwesomeIcons.circleMinus),
onPressed: () {
try {
final num newValue = num.parse(weightController.text) - 1;
weightController.text = newValue.toString();
} on FormatException {}
},
),
IconButton(
key: const Key('quickMinusSmall'),
icon: const FaIcon(FontAwesomeIcons.minus),
onPressed: () {
try {
final num newValue = num.parse(weightController.text) - 0.25;
weightController.text = newValue.toString();
} on FormatException {}
},
),
],
),
suffix: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
key: const Key('quickPlusSmall'),
icon: const FaIcon(FontAwesomeIcons.plus),
onPressed: () {
try {
final num newValue = num.parse(weightController.text) + 0.25;
weightController.text = newValue.toString();
} on FormatException {}
},
),
IconButton(
key: const Key('quickPlus'),
icon: const FaIcon(FontAwesomeIcons.circlePlus),
onPressed: () {
try {
final num newValue = num.parse(weightController.text) + 1;
weightController.text = newValue.toString();
} on FormatException {}
},
),
],
),
),
controller: weightController,
keyboardType: TextInputType.number,
onSaved: (newValue) {
@@ -113,11 +172,10 @@ class WeightForm extends StatelessWidget {
// Save the entry on the server
try {
final provider = Provider.of<BodyWeightProvider>(context, listen: false);
_weightEntry.id == null
? await Provider.of<BodyWeightProvider>(context, listen: false)
.addEntry(_weightEntry)
: await Provider.of<BodyWeightProvider>(context, listen: false)
.editEntry(_weightEntry);
? await provider.addEntry(_weightEntry)
: await provider.editEntry(_weightEntry);
} on WgerHttpException catch (error) {
if (context.mounted) {
showHttpExceptionErrorDialog(error, context);

View File

@@ -213,10 +213,10 @@ packages:
dependency: "direct dev"
description:
name: cider
sha256: "2d449e99f0c2db791bfcbf013a3c2f7c0ef48c0085d8340686ff25ce8b94fd9b"
sha256: "8e147719af74ca3df4864ba0bf1674606dfa691d659d05b93884487d96f09ede"
url: "https://pub.dev"
source: hosted
version: "0.2.5"
version: "0.2.6"
cli_util:
dependency: transitive
description:
@@ -341,10 +341,10 @@ packages:
dependency: transitive
description:
name: file
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
version: "7.0.0"
file_selector_linux:
dependency: transitive
description:
@@ -389,10 +389,10 @@ packages:
dependency: "direct main"
description:
name: fl_chart
sha256: b5e2b0f13d93f8c532b5a2786bfb44580de1f50b927bf95813fa1af617e9caf8
sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d"
url: "https://pub.dev"
source: hosted
version: "0.66.1"
version: "0.66.2"
flex_color_scheme:
dependency: "direct main"
description:
@@ -537,10 +537,10 @@ packages:
dependency: "direct main"
description:
name: flutter_typeahead
sha256: e2070dea278f09ae30885872138ccae75292b33b7af2c241fec5ceafd980c374
sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d
url: "https://pub.dev"
source: hosted
version: "5.1.0"
version: "5.2.0"
flutter_web_plugins:
dependency: transitive
description: flutter
@@ -566,10 +566,10 @@ packages:
dependency: "direct dev"
description:
name: freezed
sha256: "6c5031daae12c7072b3a87eff98983076434b4889ef2a44384d0cae3f82372ba"
sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5"
url: "https://pub.dev"
source: hosted
version: "2.4.6"
version: "2.4.7"
freezed_annotation:
dependency: "direct main"
description:
@@ -764,6 +764,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.7.1"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
url: "https://pub.dev"
source: hosted
version: "10.0.0"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
url: "https://pub.dev"
source: hosted
version: "2.0.1"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
url: "https://pub.dev"
source: hosted
version: "2.0.1"
lints:
dependency: transitive
description:
@@ -808,26 +832,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16"
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.8.0"
meta:
dependency: transitive
description:
name: meta
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.11.0"
mime:
dependency: transitive
description:
@@ -896,10 +920,10 @@ packages:
dependency: "direct main"
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
version: "1.9.0"
path_parsing:
dependency: transitive
description:
@@ -968,10 +992,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.4"
plugin_platform_interface:
dependency: transitive
description:
@@ -1032,10 +1056,10 @@ packages:
dependency: transitive
description:
name: process
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
url: "https://pub.dev"
source: hosted
version: "4.2.4"
version: "5.0.2"
provider:
dependency: "direct main"
description:
@@ -1213,10 +1237,10 @@ packages:
dependency: "direct main"
description:
name: sqlite3_flutter_libs
sha256: "90963b515721d6a71e96f438175cf43c979493ed14822860a300b69694c74eb6"
sha256: d6c31c8511c441d1f12f20b607343df1afe4eddf24a1cf85021677c8eea26060
url: "https://pub.dev"
source: hosted
version: "0.5.19+1"
version: "0.5.20"
sqlparser:
dependency: transitive
description:
@@ -1461,10 +1485,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
url: "https://pub.dev"
source: hosted
version: "11.10.0"
version: "13.0.0"
watcher:
dependency: transitive
description:
@@ -1493,10 +1517,10 @@ packages:
dependency: transitive
description:
name: webdriver
sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49"
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
win32:
dependency: transitive
description:

View File

@@ -38,7 +38,7 @@ dependencies:
equatable: ^2.0.5
flutter_calendar_carousel: ^2.4.1
flutter_html: ^3.0.0-beta.2
flutter_typeahead: ^5.1.0
flutter_typeahead: ^5.2.0
font_awesome_flutter: ^10.7.0
http: ^1.2.0
image_picker: ^1.0.6
@@ -57,12 +57,12 @@ dependencies:
carousel_slider: ^4.2.1
multi_select_flutter: ^4.1.3
flutter_svg: ^2.0.5
fl_chart: ^0.66.1
fl_chart: ^0.66.2
flutter_zxing: ^1.5.2
drift: ^2.15.0
path: ^1.8.3
path_provider: ^2.1.1
sqlite3_flutter_libs: ^0.5.19+1
sqlite3_flutter_libs: ^0.5.20
get_it: ^7.6.7
flex_seed_scheme: ^1.4.0
flex_color_scheme: ^7.3.1
@@ -79,9 +79,9 @@ dev_dependencies:
mockito: ^5.4.4
network_image_mock: ^2.1.1
flutter_lints: ^3.0.1
cider: ^0.2.4
cider: ^0.2.6
drift_dev: ^2.15.0
freezed: ^2.4.5
freezed: ^2.4.7
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View File

@@ -0,0 +1,78 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:wger/helpers/colors.dart';
void main() {
group('generateChartColors', () {
test('should generate 3 colors for 3 items using iterator', () {
final iterator = generateChartColors(3).iterator;
final expectedColors = LIST_OF_COLORS3.iterator;
while (iterator.moveNext() && expectedColors.moveNext()) {
expect(iterator.current, equals(expectedColors.current));
}
expect(iterator.moveNext(), isFalse);
expect(expectedColors.moveNext(), isFalse);
});
test('should generate 5 colors for 5 items using iterator', () {
final iterator = generateChartColors(5).iterator;
final expectedColors = LIST_OF_COLORS5.iterator;
while (iterator.moveNext() && expectedColors.moveNext()) {
expect(iterator.current, equals(expectedColors.current));
}
expect(iterator.moveNext(), isFalse);
expect(expectedColors.moveNext(), isFalse);
});
test('should generate 8 colors for 8 items using iterator', () {
final iterator = generateChartColors(8).iterator;
final expectedColors = LIST_OF_COLORS8.iterator;
while (iterator.moveNext() && expectedColors.moveNext()) {
expect(iterator.current, equals(expectedColors.current));
}
expect(iterator.moveNext(), isFalse);
expect(expectedColors.moveNext(), isFalse);
});
test('should generate 8 colors for more than 8 items using iterator', () {
final iterator = generateChartColors(10).iterator;
final expectedColors = LIST_OF_COLORS8.iterator;
while (iterator.moveNext() && expectedColors.moveNext()) {
expect(iterator.current, equals(expectedColors.current));
}
expect(iterator.moveNext(), isFalse);
expect(expectedColors.moveNext(), isFalse);
});
test('should generate 8 colors for 0 items using iterator', () {
final iterator = generateChartColors(0).iterator;
final expectedColors = LIST_OF_COLORS3.iterator;
while (iterator.moveNext() && expectedColors.moveNext()) {
expect(iterator.current, equals(expectedColors.current));
}
expect(iterator.moveNext(), isFalse);
expect(expectedColors.moveNext(), isFalse);
});
test('should generate 8 colors for negative items using iterator', () {
final iterator = generateChartColors(-5).iterator;
final expectedColors = LIST_OF_COLORS3.iterator;
while (iterator.moveNext() && expectedColors.moveNext()) {
expect(iterator.current, equals(expectedColors.current));
}
expect(iterator.moveNext(), isFalse);
expect(expectedColors.moveNext(), isFalse);
});
});
}

View File

@@ -0,0 +1,86 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (C) 2020, 2021 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* wger Workout Manager is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:wger/models/body_weight/weight_entry.dart';
import 'package:wger/widgets/weight/forms.dart';
import '../../test_data/body_weight.dart';
void main() {
Widget createWeightForm({locale = 'en', weightEntry = WeightEntry}) {
return MaterialApp(
locale: Locale(locale),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: WeightForm(weightEntry),
),
);
}
testWidgets('The form is prefilled with the data from an entry', (WidgetTester tester) async {
await tester.pumpWidget(createWeightForm(weightEntry: testWeightEntry1));
await tester.pumpAndSettle();
expect(find.text('2021-01-01'), findsOneWidget);
expect(find.text('80'), findsOneWidget);
});
testWidgets('It is possible to quick-change the weight', (WidgetTester tester) async {
await tester.pumpWidget(createWeightForm(weightEntry: testWeightEntry1));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('quickMinus')));
expect(find.text('79'), findsOneWidget);
await tester.tap(find.byKey(const Key('quickMinusSmall')));
expect(find.text('78.75'), findsOneWidget);
await tester.tap(find.byKey(const Key('quickPlus')));
expect(find.text('79.75'), findsOneWidget);
await tester.tap(find.byKey(const Key('quickPlusSmall')));
expect(find.text('80.0'), findsOneWidget);
});
testWidgets("Entering garbage doesn't break the quick-change", (WidgetTester tester) async {
await tester.pumpWidget(createWeightForm(weightEntry: testWeightEntry1));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('weightInput')), 'shiba inu');
await tester.tap(find.byKey(const Key('quickMinus')));
expect(find.text('shiba inu'), findsOneWidget);
await tester.tap(find.byKey(const Key('quickMinusSmall')));
expect(find.text('shiba inu'), findsOneWidget);
await tester.tap(find.byKey(const Key('quickPlus')));
expect(find.text('shiba inu'), findsOneWidget);
await tester.tap(find.byKey(const Key('quickPlusSmall')));
expect(find.text('shiba inu'), findsOneWidget);
});
testWidgets('Widget works if there is no last entry', (WidgetTester tester) async {
await tester.pumpWidget(createWeightForm(weightEntry: null));
await tester.pumpAndSettle();
});
}

View File

@@ -41,6 +41,7 @@ void main() {
setUp(() {
mockWeightProvider = MockBodyWeightProvider();
when(mockWeightProvider.items).thenReturn(getWeightEntries());
when(mockWeightProvider.getNewestEntry()).thenReturn(null);
mockUserProvider = MockUserProvider();
when(mockUserProvider.profile).thenReturn(tProfile1);

View File

@@ -18,9 +18,9 @@
import 'package:wger/models/body_weight/weight_entry.dart';
final weightEntry1 = WeightEntry(id: 1, weight: 80, date: DateTime(2021, 01, 01));
final weightEntry2 = WeightEntry(id: 2, weight: 81, date: DateTime(2021, 01, 10));
final testWeightEntry1 = WeightEntry(id: 1, weight: 80, date: DateTime(2021, 01, 01));
final testWeightEntry2 = WeightEntry(id: 2, weight: 81, date: DateTime(2021, 01, 10));
List<WeightEntry> getWeightEntries() {
return [weightEntry1, weightEntry2];
return [testWeightEntry1, testWeightEntry2];
}