From c443901714539219914fb9ed42709433b9b845cd Mon Sep 17 00:00:00 2001 From: doman Date: Fri, 27 Oct 2023 17:10:09 +0200 Subject: [PATCH] [preset] GO --- lib/client.dart | 48 +++++++++++++++++ lib/models/preset.dart | 28 ++++++++++ lib/screens/add_meal.dart | 109 ++++++++++++++++++++++++++++++++++---- lib/widgets/meal.dart | 71 +++++++++++++++++++++++++ lib/widgets/preset.dart | 40 ++++++++++++++ 5 files changed, 286 insertions(+), 10 deletions(-) create mode 100644 lib/models/preset.dart create mode 100644 lib/widgets/preset.dart diff --git a/lib/client.dart b/lib/client.dart index 66ca0d1..50132f8 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -2,6 +2,7 @@ import 'package:http/http.dart' as http; import 'dart:convert'; import 'dart:html'; import 'package:intl/intl.dart'; +import 'package:fooder/models/meal.dart'; class ApiClient { final String baseUrl; @@ -86,6 +87,24 @@ class ApiClient { return _jsonDecode(response); } + Future postNoResult(String path, Map body, + {bool forLogin = false, bool empty = false}) async { + final response = await httpClient.post( + Uri.parse('$baseUrl$path'), + body: jsonEncode(body), + headers: headers(forLogin: forLogin), + ); + + if (response.statusCode == 401) { + await refresh(); + return await postNoResult(path, body, forLogin: forLogin); + } + + if (response.statusCode != 200) { + throw Exception('Response returned status code: ${response.statusCode}'); + } + } + Future delete(String path) async { final response = await httpClient.delete( Uri.parse('$baseUrl$path'), @@ -187,6 +206,11 @@ class ApiClient { return response; } + Future> getPresets(String? q) async { + var response = await get("/preset?${Uri(queryParameters: {"q": q}).query}"); + return response; + } + Future addEntry({ required double grams, required int productId, @@ -204,6 +228,14 @@ class ApiClient { await delete("/entry/$id"); } + Future deleteMeal(int id) async { + await delete("/meal/$id"); + } + + Future deletePreset(int id) async { + await delete("/preset/$id"); + } + Future updateEntry( int id, { required double grams, @@ -242,6 +274,16 @@ class ApiClient { }); } + Future addMealFromPreset( + {required String name, required int diaryId, required int order, required int presetId}) async { + await post("/meal/from_preset", { + "name": name, + "diary_id": diaryId, + "order": order, + "preset_id": presetId, + }); + } + Future> addProduct({ required String name, required double protein, @@ -258,4 +300,10 @@ class ApiClient { }); return response; } + + Future saveMeal(Meal meal, String name) async { + await postNoResult("/meal/${meal.id}/save", { + "name": name, + }); + } } diff --git a/lib/models/preset.dart b/lib/models/preset.dart new file mode 100644 index 0000000..5869a22 --- /dev/null +++ b/lib/models/preset.dart @@ -0,0 +1,28 @@ +class Preset { + final int id; + final String name; + final double calories; + final double protein; + final double carb; + final double fat; + final double fiber; + + Preset({ + required this.id, + required this.name, + required this.calories, + required this.protein, + required this.carb, + required this.fat, + required this.fiber, + }); + + Preset.fromJson(Map map) + : id = map['id'] as int, + name = map['name'] as String, + calories = map['calories'] as double, + protein = map['protein'] as double, + carb = map['carb'] as double, + fat = map['fat'] as double, + fiber = map['fiber'] as double; +} diff --git a/lib/screens/add_meal.dart b/lib/screens/add_meal.dart index 755cc54..084e18d 100644 --- a/lib/screens/add_meal.dart +++ b/lib/screens/add_meal.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:fooder/screens/based.dart'; import 'package:fooder/models/diary.dart'; +import 'package:fooder/models/preset.dart'; +import 'package:fooder/widgets/preset.dart'; class AddMealScreen extends BasedScreen { final Diary diary; @@ -14,6 +16,20 @@ class AddMealScreen extends BasedScreen { class _AddMealScreen extends State { final nameController = TextEditingController(); + final presetNameController = TextEditingController(); + bool nameChanged = false; + List presets = []; + + Future _getPresets() async { + var presetsMap = + await widget.apiClient.getPresets(presetNameController.text); + + setState(() { + presets = (presetsMap['presets'] as List) + .map((e) => Preset.fromJson(e as Map)) + .toList(); + }); + } @override void initState() { @@ -21,6 +37,7 @@ class _AddMealScreen extends State { setState(() { nameController.text = "Meal ${widget.diary.meals.length}"; }); + _getPresets(); } @override @@ -53,26 +70,98 @@ class _AddMealScreen extends State { popMeDaddy(); } + Future _deletePreset(context, Preset preset) async { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Confirm deletion of the preset'), + actions: [ + IconButton( + icon: const Icon(Icons.cancel), + onPressed: () { + Navigator.pop(context); + }, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + widget.apiClient.deletePreset(preset.id); + Navigator.pop(context); + }, + ), + ], + ); + }, + ); + } + + Future _addMealFromPreset() async { + if (presets.length != 1) { + _addMeal(); + return; + } + + await widget.apiClient.addMealFromPreset( + name: nameChanged ? nameController.text : presets[0].name, + diaryId: widget.diary.id, + order: widget.diary.meals.length, + presetId: presets[0].id, + ); + popMeDaddy(); + } + @override Widget build(BuildContext context) { + _getPresets(); + return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text("πŸ…΅πŸ…ΎπŸ…ΎπŸ…³πŸ…΄πŸ†", style: logoStyle(context)), ), body: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 720), - padding: const EdgeInsets.all(10), - child: TextFormField( - decoration: const InputDecoration( - labelText: 'Meal name', - ), - controller: nameController, + child: Container( + constraints: const BoxConstraints(maxWidth: 720), + padding: const EdgeInsets.all(10), + child: ListView(children: [ + TextFormField( + decoration: const InputDecoration( + labelText: 'Meal name', + ), + controller: nameController, + onChanged: (_) => setState(() { + nameChanged = true; + }), + ), + TextFormField( + decoration: const InputDecoration( + hintText: 'Search presets', + ), + controller: presetNameController, + onChanged: (_) => _getPresets(), + ), + for (var preset in presets) + ListTile( + onTap: () { + setState(() { + presets = [preset]; + presetNameController.text = preset.name; + }); + _addMealFromPreset(); + }, + onLongPress: () { + _deletePreset(context, preset); + }, + title: PresetWidget( + preset: preset, + ), + ), + ]), ), - )), + ), floatingActionButton: FloatingActionButton( - onPressed: _addMeal, + onPressed: _addMealFromPreset, child: const Icon(Icons.add), ), ); diff --git a/lib/widgets/meal.dart b/lib/widgets/meal.dart index 42e9c9a..1fb8b48 100644 --- a/lib/widgets/meal.dart +++ b/lib/widgets/meal.dart @@ -17,6 +17,69 @@ class MealWidget extends StatelessWidget { required this.apiClient, required this.refreshParent}); + Future saveMeal(context) async { + TextEditingController textFieldController = TextEditingController(); + + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Save Meal'), + content: TextField( + controller: textFieldController, + decoration: const InputDecoration(hintText: "Meal template name"), + ), + actions: [ + IconButton( + icon: const Icon(Icons.cancel), + onPressed: () { + Navigator.pop(context); + }, + ), + IconButton( + icon: const Icon(Icons.save), + onPressed: () { + apiClient.saveMeal(meal, textFieldController.text); + Navigator.pop(context); + }, + ), + ], + ); + }, + ); + } + + Future _deleteMeal(Meal meal) async { + await apiClient.deleteMeal(meal.id); + refreshParent(); + } + + Future deleteMeal(context) async { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Confirm deletion of the meal'), + actions: [ + IconButton( + icon: const Icon(Icons.cancel), + onPressed: () { + Navigator.pop(context); + }, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + _deleteMeal(meal); + Navigator.pop(context); + }, + ), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { return Card( @@ -36,6 +99,14 @@ class MealWidget extends StatelessWidget { ), ), ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () {deleteMeal(context);}, + ), + IconButton( + icon: const Icon(Icons.save), + onPressed: () {saveMeal(context);}, + ), Text("${meal.calories.toStringAsFixed(1)} kcal"), ], ), diff --git a/lib/widgets/preset.dart b/lib/widgets/preset.dart new file mode 100644 index 0000000..0f4e0f8 --- /dev/null +++ b/lib/widgets/preset.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:fooder/models/preset.dart'; +import 'package:fooder/widgets/macro.dart'; +import 'dart:core'; + +class PresetWidget extends StatelessWidget { + final Preset preset; + + const PresetWidget({super.key, required this.preset}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + preset.name, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + Text("${preset.calories.toStringAsFixed(1)} kcal"), + ], + ), + MacroWidget( + protein: preset.protein, + carb: preset.carb, + fat: preset.fat, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ], + ), + ); + } +}