mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Merge branch 'master' into feature/nutrition-goals
This commit is contained in:
2
.github/workflows/build-release.yml
vendored
2
.github/workflows/build-release.yml
vendored
@@ -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: |
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/linter.yml
vendored
2
.github/workflows/linter.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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": {},
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
80
pubspec.lock
80
pubspec.lock
@@ -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:
|
||||
|
||||
10
pubspec.yaml
10
pubspec.yaml
@@ -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
|
||||
|
||||
78
test/helpers/colors_test.dart
Normal file
78
test/helpers/colors_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
86
test/weight/weight_form_test.dart
Normal file
86
test/weight/weight_form_test.dart
Normal 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();
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user