diff --git a/lib/helpers/json.dart b/lib/helpers/json.dart new file mode 100644 index 00000000..8488dab3 --- /dev/null +++ b/lib/helpers/json.dart @@ -0,0 +1,7 @@ +num toNum(String e) { + return num.parse(e); +} + +String toString(num e) { + return e.toString(); +} diff --git a/lib/main.dart b/lib/main.dart index 8e716e2c..af9a9708 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; import 'package:wger/locale/locales.dart'; +import 'package:wger/providers/body_weight.dart'; import 'package:wger/providers/exercises.dart'; import 'package:wger/providers/nutritional_plans.dart'; import 'package:wger/providers/workout_plans.dart'; @@ -53,6 +54,13 @@ class MyApp extends StatelessWidget { auth, previous == null ? [] : previous.items, ), + ), + ChangeNotifierProxyProvider( + create: null, // TODO: Create is required but it can be null?? + update: (context, value, previous) => BodyWeight( + auth, + previous == null ? [] : previous.items, + ), ) ], child: MaterialApp( diff --git a/lib/models/body_weight/weight_entry.dart b/lib/models/body_weight/weight_entry.dart new file mode 100644 index 00000000..c81f7845 --- /dev/null +++ b/lib/models/body_weight/weight_entry.dart @@ -0,0 +1,27 @@ +import 'package:flutter/foundation.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:wger/helpers/json.dart'; + +part 'weight_entry.g.dart'; + +@JsonSerializable() +class WeightEntry { + @JsonKey(required: true) + final int id; + + @JsonKey(required: true, fromJson: toNum, toJson: toString) + final num weight; + + @JsonKey(required: true) + final DateTime date; + + WeightEntry({ + @required this.id, + @required this.weight, + @required this.date, + }); + + // Boilerplate + factory WeightEntry.fromJson(Map json) => _$WeightEntryFromJson(json); + Map toJson() => _$WeightEntryToJson(this); +} diff --git a/lib/models/body_weight/weight_entry.g.dart b/lib/models/body_weight/weight_entry.g.dart new file mode 100644 index 00000000..52af3236 --- /dev/null +++ b/lib/models/body_weight/weight_entry.g.dart @@ -0,0 +1,22 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'weight_entry.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +WeightEntry _$WeightEntryFromJson(Map json) { + $checkKeys(json, requiredKeys: const ['id', 'weight', 'date']); + return WeightEntry( + id: json['id'] as int, + weight: toNum(json['weight'] as String), + date: json['date'] == null ? null : DateTime.parse(json['date'] as String), + ); +} + +Map _$WeightEntryToJson(WeightEntry instance) => { + 'id': instance.id, + 'weight': instance.weight, + 'date': instance.date?.toIso8601String(), + }; diff --git a/lib/providers/body_weight.dart b/lib/providers/body_weight.dart new file mode 100644 index 00000000..d362c728 --- /dev/null +++ b/lib/providers/body_weight.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:wger/models/body_weight/weight_entry.dart'; +import 'package:wger/providers/auth.dart'; + +class BodyWeight with ChangeNotifier { + static const nutritionPlansUrl = '/api/v2/weightentry/'; + + String _url; + Auth _auth; + List _entries = []; + + BodyWeight(Auth auth, List entries) { + this._auth = auth; + this._entries = entries; + this._url = auth.serverUrl + nutritionPlansUrl; + } + + List get items { + return [..._entries]; + } + + WeightEntry findById(int id) { + return _entries.firstWhere((plan) => plan.id == id); + } + + Future fetchAndSetEntries() async { + final response = await http.get( + _url, + headers: {'Authorization': 'Token ${_auth.token}'}, + ); + final extractedData = json.decode(response.body) as Map; + + final List loadedEntries = []; + if (loadedEntries == null) { + return; + } + + for (final entry in extractedData['results']) { + loadedEntries.add(WeightEntry.fromJson(entry)); + } + + _entries = loadedEntries; + notifyListeners(); + } + + Future addEntry(WeightEntry entry) async { + try { + final response = await http.post( + _url, + headers: { + 'Authorization': 'Token ${_auth.token}', + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: json.encode(entry.toJson()), + ); + _entries.insert(0, WeightEntry.fromJson(json.decode(response.body))); + notifyListeners(); + } catch (error) { + log(error); + throw error; + } + } + + Future deleteEntry(int id) async { + final url = '$_url$id/'; + final existingPlanIndex = _entries.indexWhere((element) => element.id == id); + var existingPlan = _entries[existingPlanIndex]; + _entries.removeAt(existingPlanIndex); + notifyListeners(); + + final response = await http.delete( + url, + headers: {'Authorization': 'Token ${_auth.token}'}, + ); + if (response.statusCode >= 400) { + _entries.insert(existingPlanIndex, existingPlan); + notifyListeners(); + //throw HttpException(); + } + existingPlan = null; + } +} diff --git a/lib/screens/weight_screen.dart b/lib/screens/weight_screen.dart index 3170546f..539b8b58 100644 --- a/lib/screens/weight_screen.dart +++ b/lib/screens/weight_screen.dart @@ -1,9 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/models/body_weight/weight_entry.dart'; +import 'package:wger/providers/body_weight.dart'; import 'package:wger/widgets/app_drawer.dart'; +import 'package:wger/widgets/weight/entries_list.dart'; class WeightScreen extends StatelessWidget { static const routeName = '/weight'; + Future _refreshWeightEntries(BuildContext context) async { + await Provider.of(context, listen: false).fetchAndSetEntries(); + } + Widget getAppBar() { return AppBar( title: Text('Weight'), @@ -22,9 +30,27 @@ class WeightScreen extends StatelessWidget { appBar: getAppBar(), drawer: AppDrawer(), floatingActionButton: FloatingActionButton( - onPressed: () {}, + onPressed: () { + Provider.of(context, listen: false).addEntry( + WeightEntry( + date: DateTime.now(), + weight: 80, + ), + ); + }, child: const Icon(Icons.add), ), + body: FutureBuilder( + future: _refreshWeightEntries(context), + builder: (context, snapshot) => snapshot.connectionState == ConnectionState.waiting + ? Center(child: CircularProgressIndicator()) + : RefreshIndicator( + onRefresh: () => _refreshWeightEntries(context), + child: Consumer( + builder: (context, productsData, child) => WeightEntriesList(), + ), + ), + ), ); } } diff --git a/lib/widgets/weight/entries_list.dart b/lib/widgets/weight/entries_list.dart new file mode 100644 index 00000000..75a3e986 --- /dev/null +++ b/lib/widgets/weight/entries_list.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/providers/body_weight.dart'; + +class WeightEntriesList extends StatelessWidget { + @override + Widget build(BuildContext context) { + final weightEntriesData = Provider.of(context); + return ListView.builder( + padding: const EdgeInsets.all(10.0), + itemCount: weightEntriesData.items.length, + itemBuilder: (context, index) { + final currentEntry = weightEntriesData.items[index]; + return Dismissible( + key: Key(currentEntry.id.toString()), + onDismissed: (direction) { + // Delete workout from DB + Provider.of(context, listen: false).deleteEntry(currentEntry.id); + + // and inform the user + Scaffold.of(context).showSnackBar( + SnackBar( + content: Text( + "Weight entry for the ${currentEntry.date}", + textAlign: TextAlign.center, + ), + ), + ); + }, + background: Container( + color: Theme.of(context).errorColor, + alignment: Alignment.centerRight, + padding: EdgeInsets.only(right: 20), + margin: EdgeInsets.symmetric( + horizontal: 4, + vertical: 4, + ), + child: Icon( + Icons.delete, + color: Colors.white, + ), + ), + direction: DismissDirection.endToStart, + child: Card( + child: ListTile( + //onTap: () => Navigator.of(context).pushNamed( + // WorkoutPlanScreen.routeName, + // arguments: currentPlan, + //), + onTap: () {}, + title: Text( + DateFormat('dd.MM.yyyy').format(currentEntry.date).toString(), + ), + subtitle: Text('${currentEntry.weight} kg'), + ), + ), + ); + }, + ); + } +} diff --git a/lib/widgets/workouts/workout_plans_list.dart b/lib/widgets/workouts/workout_plans_list.dart index be0350e3..3b423ebd 100644 --- a/lib/widgets/workouts/workout_plans_list.dart +++ b/lib/widgets/workouts/workout_plans_list.dart @@ -49,7 +49,6 @@ class WorkoutPlansList extends StatelessWidget { WorkoutPlanScreen.routeName, arguments: currentWorkout, ), - leading: Icon(Icons.edit), title: Text( DateFormat('dd.MM.yyyy').format(currentWorkout.creationDate).toString(), ),