diff --git a/lib/client.dart b/lib/client.dart index 0ecc3f5..18f6679 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -76,7 +76,7 @@ class ApiClient { } - Future> delete(String path) async { + Future delete(String path) async { final response = await httpClient.delete( Uri.parse('$baseUrl$path'), headers: headers(), @@ -85,8 +85,6 @@ class ApiClient { if (response.statusCode != 200) { throw Exception('Response returned status code: ${response.statusCode}'); } - - return _jsonDecode(response); } Future> patch(String path, Map body) async { @@ -182,4 +180,22 @@ class ApiClient { }; await post("/entry", entry); } + + Future deleteEntry(int id) async { + await delete("/entry/$id"); + } + + Future updateEntry(int id, { + required double grams, + required int productId, + required int mealId, + } + ) async { + var entry = { + "grams": grams, + "product_id": productId, + "meal_id": mealId, + }; + await patch("/entry/$id", entry); + } } diff --git a/lib/screens/add_entry.dart b/lib/screens/add_entry.dart index d66a5fd..35d2249 100644 --- a/lib/screens/add_entry.dart +++ b/lib/screens/add_entry.dart @@ -65,7 +65,6 @@ class _AddEntryScreen extends State { try { double.parse(gramsController.text); } catch (e) { - print(e); showError("Grams must be a number"); return; } @@ -106,10 +105,10 @@ class _AddEntryScreen extends State { ), for (var product in products) ListTile( - onTap: () { - setState(() { + onTap: () { + setState(() { products = [product]; - }); + }); }, title: ProductWidget( product: product, diff --git a/lib/screens/edit_entry.dart b/lib/screens/edit_entry.dart new file mode 100644 index 0000000..9510c45 --- /dev/null +++ b/lib/screens/edit_entry.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:fooder_web/screens/based.dart'; +import 'package:fooder_web/models/product.dart'; +import 'package:fooder_web/models/entry.dart'; +import 'package:fooder_web/widgets/product.dart'; + + +class EditEntryScreen extends BasedScreen { + final Entry entry; + + const EditEntryScreen({super.key, required super.apiClient, required this.entry}); + + @override + State createState() => _EditEntryScreen(); +} + + +class _EditEntryScreen extends State { + final gramsController = TextEditingController(); + final productNameController = TextEditingController(); + List products = []; + + @override + void dispose() { + gramsController.dispose(); + productNameController.dispose(); + super.dispose(); + } + + void popMeDady() { + Navigator.pop(context); + } + + @override + void initState () { + super.initState(); + setState(() { + gramsController.text = widget.entry.grams.toString(); + productNameController.text = widget.entry.product.name; + products = [widget.entry.product]; + }); + } + + Future _getProducts() async { + var productsMap = await widget.apiClient.getProducts(productNameController.text); + setState(() { + products = (productsMap['products'] as List).map((e) => Product.fromJson(e as Map)).toList(); + }); + } + + void showError(String message) + { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message, textAlign: TextAlign.center), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + + Future _saveEntry() async { + if (products.length != 1) { + showError("Pick product first"); + return; + } + + try { + double.parse(gramsController.text); + } catch (e) { + showError("Grams must be a number"); + return; + } + + await widget.apiClient.updateEntry( + widget.entry.id, + grams: double.parse(gramsController.text), + productId: products[0].id, + mealId: widget.entry.mealId, + ); + popMeDady(); + } + + Future _deleteEntry() async { + await widget.apiClient.deleteEntry(widget.entry.id); + popMeDady(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text("πŸ…΅πŸ…ΎπŸ…ΎπŸ…³πŸ…΄πŸ†"), + ), + body: Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 720), + padding: const EdgeInsets.all(10), + child: ListView( + children: [ + TextFormField( + decoration: const InputDecoration( + labelText: 'Grams', + ), + controller: gramsController, + ), + TextFormField( + decoration: const InputDecoration( + labelText: 'Product name', + ), + controller: productNameController, + onChanged: (_) => _getProducts(), + ), + for (var product in products) + ListTile( + onTap: () { + setState(() { + products = [product]; + }); + }, + title: ProductWidget( + product: product, + ), + ), + ] + ) + ), + ), + floatingActionButton: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton( + onPressed: _deleteEntry, + heroTag: null, + child: const Icon(Icons.delete), + ), + FloatingActionButton( + onPressed: _saveEntry, + heroTag: null, + child: const Icon(Icons.save), + ), + ], + ), + ); + } +} diff --git a/lib/screens/main.dart b/lib/screens/main.dart index 6876837..b351175 100644 --- a/lib/screens/main.dart +++ b/lib/screens/main.dart @@ -69,7 +69,7 @@ class _MainScreen extends State { content = Container( constraints: const BoxConstraints(maxWidth: 720), padding: const EdgeInsets.all(10), - child: DiaryWidget(diary: diary!), + child: DiaryWidget(diary: diary!, apiClient: widget.apiClient, refreshParent: _asyncInitState), ); title = Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/widgets/diary.dart b/lib/widgets/diary.dart index 4eb8566..fb803a1 100644 --- a/lib/widgets/diary.dart +++ b/lib/widgets/diary.dart @@ -1,55 +1,67 @@ import 'package:flutter/material.dart'; import 'package:fooder_web/models/diary.dart'; import 'package:fooder_web/widgets/meal.dart'; +import 'package:fooder_web/widgets/macro.dart'; +import 'package:fooder_web/client.dart'; import 'dart:core'; class DiaryWidget extends StatelessWidget { final Diary diary; + final ApiClient apiClient; + final Function() refreshParent; - const DiaryWidget({super.key, required this.diary}); + const DiaryWidget({super.key, required this.diary, required this.apiClient, required this.refreshParent}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(8), - child: ListView( - padding: const EdgeInsets.all(8), - children: [ - for (var meal in diary.meals) - MealWidget( - meal: meal, - ), - Card( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "carb: ${diary.carb.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - Text( - "fat: ${diary.fat.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - Text( - "protein: ${diary.protein.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - Text( - "calories: ${diary.calories.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.primary), - ), - ], + child: CustomScrollView( + slivers: [ + SliverAppBar( + title: Row( + children: [ + const Spacer(), + Text( + "${diary.calories.toStringAsFixed(1)} kcal", + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + color: Theme.of(context).colorScheme.onSecondary, + fontWeight: FontWeight.bold, ), - ], + ), + ] + ), + expandedHeight: 128, + backgroundColor: Theme.of(context).colorScheme.secondary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + floating: true, + flexibleSpace: FlexibleSpaceBar( + title: MacroWidget( + protein: diary.protein, + carb: diary.carb, + fat: diary.fat, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSecondary, + fontWeight: FontWeight.bold, + ), ), ), - ) + ), + SliverList( + delegate: SliverChildListDelegate( + [ + for (var meal in diary.meals) + MealWidget( + meal: meal, + apiClient: apiClient, + refreshParent: refreshParent, + ), + ], + ), + ), ], ), ); diff --git a/lib/widgets/entry.dart b/lib/widgets/entry.dart index 8e3815d..5b2ea19 100644 --- a/lib/widgets/entry.dart +++ b/lib/widgets/entry.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:fooder_web/models/entry.dart'; +import 'package:fooder_web/widgets/macro.dart'; import 'dart:core'; @@ -22,29 +23,17 @@ class EntryWidget extends StatelessWidget { style: Theme.of(context).textTheme.titleLarge, ), ), - Text("${entry.calories.toStringAsFixed(2)} kcal"), + Text("${entry.calories.toStringAsFixed(1)} kcal"), ], ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "carb: ${entry.carb.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - Text( - "fat: ${entry.fat.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - Text( - "protein: ${entry.protein.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - Text( - "amount: ${entry.grams.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - ], + MacroWidget( + protein: entry.protein, + carb: entry.carb, + fat: entry.fat, + amount: entry.grams, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), ), ], ), diff --git a/lib/widgets/macro.dart b/lib/widgets/macro.dart new file mode 100644 index 0000000..e6ee093 --- /dev/null +++ b/lib/widgets/macro.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + + +class MacroWidget extends StatelessWidget { + final double? amount; + final double? calories; + final double protein; + final double carb; + final double fat; + final TextStyle style; + + const MacroWidget({ + Key? key, + this.calories, + this.amount, + required this.protein, + required this.carb, + required this.fat, + required this.style, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var elements = [ + Expanded( + flex: 1, + child: Text( + "C: ${carb.toStringAsFixed(1)} g", + style: style, + textAlign: TextAlign.center, + ), + ), + Expanded( + flex: 1, + child: Text( + "F: ${fat.toStringAsFixed(1)} g", + style: style, + textAlign: TextAlign.center, + ), + ), + Expanded( + flex: 1, + child: Text( + "P: ${protein.toStringAsFixed(1)} g", + style: style, + textAlign: TextAlign.center, + ), + ), + ]; + + if (calories != null) { + elements.add( + Expanded( + flex: 1, + child: Text( + "${calories!.toStringAsFixed(1)} kcal", + style: style, + textAlign: TextAlign.center, + ), + ), + ); + } + + if (amount != null) { + elements.add( + Expanded( + flex: 1, + child: Text( + "${amount!.toStringAsFixed(1)} g", + style: style, + textAlign: TextAlign.center, + ), + ), + ); + } + + if (amount == null && calories == null) { + elements.add( + Expanded( + flex: 1, + child: Text( + "", + style: style, + textAlign: TextAlign.center, + ), + ), + ); + } + + return Padding( + padding: const EdgeInsets.only( + top: 4.0, + bottom: 4.0, + left: 8.0, + right: 8.0, + ), + child: Row( + children: elements, + ), + ); + } +} diff --git a/lib/widgets/meal.dart b/lib/widgets/meal.dart index 7799918..542884b 100644 --- a/lib/widgets/meal.dart +++ b/lib/widgets/meal.dart @@ -1,13 +1,18 @@ import 'package:flutter/material.dart'; import 'package:fooder_web/models/meal.dart'; import 'package:fooder_web/widgets/entry.dart'; +import 'package:fooder_web/widgets/macro.dart'; +import 'package:fooder_web/screens/edit_entry.dart'; +import 'package:fooder_web/client.dart'; import 'dart:core'; class MealWidget extends StatelessWidget { final Meal meal; + final ApiClient apiClient; + final Function() refreshParent; - const MealWidget({super.key, required this.meal}); + const MealWidget({super.key, required this.meal, required this.apiClient, required this.refreshParent}); @override Widget build(BuildContext context) { @@ -26,33 +31,39 @@ class MealWidget extends StatelessWidget { style: Theme.of(context).textTheme.titleLarge, ), ), - Text("${meal.calories.toStringAsFixed(2)} kcal"), + Text("${meal.calories.toStringAsFixed(1)} kcal"), ], ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "carb: ${meal.carb.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - Text( - "fat: ${meal.fat.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - Text( - "protein: ${meal.protein.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - ], + MacroWidget( + protein: meal.protein, + carb: meal.carb, + fat: meal.fat, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), ), ], ), children: [ for (var entry in meal.entries) - EntryWidget( - entry: entry, + ListTile( + title: EntryWidget( + entry: entry, ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EditEntryScreen( + apiClient: apiClient, + entry: entry, + ), + ), + ).then((_) { + refreshParent(); + }); + }, + ) ], ), ), diff --git a/lib/widgets/product.dart b/lib/widgets/product.dart index 83c6270..c434087 100644 --- a/lib/widgets/product.dart +++ b/lib/widgets/product.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:fooder_web/models/product.dart'; +import 'package:fooder_web/widgets/macro.dart'; import 'dart:core'; @@ -22,25 +23,17 @@ class ProductWidget extends StatelessWidget { style: Theme.of(context).textTheme.titleLarge, ), ), - Text("${product.calories.toStringAsFixed(2)} kcal"), + Text("${product.calories.toStringAsFixed(1)} kcal"), ], ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "carb: ${product.carb.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - Text( - "fat: ${product.fat.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - Text( - "protein: ${product.protein.toStringAsFixed(2)}", - style: TextStyle(color: Theme.of(context).colorScheme.secondary), - ), - ], + MacroWidget( + protein: product.protein, + carb: product.carb, + fat: product.fat, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), ), ], ),