[preset] GO

This commit is contained in:
doman 2023-10-27 17:10:09 +02:00
parent 5a893f80f0
commit c443901714
5 changed files with 286 additions and 10 deletions

View file

@ -2,6 +2,7 @@ import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
import 'dart:html'; import 'dart:html';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:fooder/models/meal.dart';
class ApiClient { class ApiClient {
final String baseUrl; final String baseUrl;
@ -86,6 +87,24 @@ class ApiClient {
return _jsonDecode(response); return _jsonDecode(response);
} }
Future<void> postNoResult(String path, Map<String, dynamic> 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<void> delete(String path) async { Future<void> delete(String path) async {
final response = await httpClient.delete( final response = await httpClient.delete(
Uri.parse('$baseUrl$path'), Uri.parse('$baseUrl$path'),
@ -187,6 +206,11 @@ class ApiClient {
return response; return response;
} }
Future<Map<String, dynamic>> getPresets(String? q) async {
var response = await get("/preset?${Uri(queryParameters: {"q": q}).query}");
return response;
}
Future<void> addEntry({ Future<void> addEntry({
required double grams, required double grams,
required int productId, required int productId,
@ -204,6 +228,14 @@ class ApiClient {
await delete("/entry/$id"); await delete("/entry/$id");
} }
Future<void> deleteMeal(int id) async {
await delete("/meal/$id");
}
Future<void> deletePreset(int id) async {
await delete("/preset/$id");
}
Future<void> updateEntry( Future<void> updateEntry(
int id, { int id, {
required double grams, required double grams,
@ -242,6 +274,16 @@ class ApiClient {
}); });
} }
Future<void> 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<Map<String, dynamic>> addProduct({ Future<Map<String, dynamic>> addProduct({
required String name, required String name,
required double protein, required double protein,
@ -258,4 +300,10 @@ class ApiClient {
}); });
return response; return response;
} }
Future<void> saveMeal(Meal meal, String name) async {
await postNoResult("/meal/${meal.id}/save", {
"name": name,
});
}
} }

28
lib/models/preset.dart Normal file
View file

@ -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<String, dynamic> 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;
}

View file

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fooder/screens/based.dart'; import 'package:fooder/screens/based.dart';
import 'package:fooder/models/diary.dart'; import 'package:fooder/models/diary.dart';
import 'package:fooder/models/preset.dart';
import 'package:fooder/widgets/preset.dart';
class AddMealScreen extends BasedScreen { class AddMealScreen extends BasedScreen {
final Diary diary; final Diary diary;
@ -14,6 +16,20 @@ class AddMealScreen extends BasedScreen {
class _AddMealScreen extends State<AddMealScreen> { class _AddMealScreen extends State<AddMealScreen> {
final nameController = TextEditingController(); final nameController = TextEditingController();
final presetNameController = TextEditingController();
bool nameChanged = false;
List<Preset> presets = [];
Future<void> _getPresets() async {
var presetsMap =
await widget.apiClient.getPresets(presetNameController.text);
setState(() {
presets = (presetsMap['presets'] as List<dynamic>)
.map((e) => Preset.fromJson(e as Map<String, dynamic>))
.toList();
});
}
@override @override
void initState() { void initState() {
@ -21,6 +37,7 @@ class _AddMealScreen extends State<AddMealScreen> {
setState(() { setState(() {
nameController.text = "Meal ${widget.diary.meals.length}"; nameController.text = "Meal ${widget.diary.meals.length}";
}); });
_getPresets();
} }
@override @override
@ -53,26 +70,98 @@ class _AddMealScreen extends State<AddMealScreen> {
popMeDaddy(); popMeDaddy();
} }
Future<void> _deletePreset(context, Preset preset) async {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Confirm deletion of the preset'),
actions: <Widget>[
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<void> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_getPresets();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary, backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text("🅵🅾🅾🅳🅴🆁", style: logoStyle(context)), title: Text("🅵🅾🅾🅳🅴🆁", style: logoStyle(context)),
), ),
body: Center( body: Center(
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 720), constraints: const BoxConstraints(maxWidth: 720),
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: TextFormField( child: ListView(children: <Widget>[
decoration: const InputDecoration( TextFormField(
labelText: 'Meal name', decoration: const InputDecoration(
), labelText: 'Meal name',
controller: nameController, ),
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( floatingActionButton: FloatingActionButton(
onPressed: _addMeal, onPressed: _addMealFromPreset,
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),
); );

View file

@ -17,6 +17,69 @@ class MealWidget extends StatelessWidget {
required this.apiClient, required this.apiClient,
required this.refreshParent}); required this.refreshParent});
Future<void> 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: <Widget>[
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<void> _deleteMeal(Meal meal) async {
await apiClient.deleteMeal(meal.id);
refreshParent();
}
Future<void> deleteMeal(context) async {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Confirm deletion of the meal'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.cancel),
onPressed: () {
Navigator.pop(context);
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
_deleteMeal(meal);
Navigator.pop(context);
},
),
],
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( 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"), Text("${meal.calories.toStringAsFixed(1)} kcal"),
], ],
), ),

40
lib/widgets/preset.dart Normal file
View file

@ -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: <Widget>[
Row(
children: <Widget>[
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,
),
),
],
),
);
}
}