Allow users to quickly increment the body weight

This commit is contained in:
Roland Geider
2024-02-17 18:48:48 +01:00
parent 3aa92338fb
commit 9c7b65bc49
10 changed files with 215 additions and 36 deletions

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

@@ -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

@@ -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

@@ -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:
@@ -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:
@@ -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

@@ -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];
}