[preset] GO
This commit is contained in:
parent
5a893f80f0
commit
c443901714
5 changed files with 286 additions and 10 deletions
|
@ -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<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 {
|
||||
final response = await httpClient.delete(
|
||||
Uri.parse('$baseUrl$path'),
|
||||
|
@ -187,6 +206,11 @@ class ApiClient {
|
|||
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({
|
||||
required double grams,
|
||||
required int productId,
|
||||
|
@ -204,6 +228,14 @@ class ApiClient {
|
|||
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(
|
||||
int id, {
|
||||
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({
|
||||
required String name,
|
||||
required double protein,
|
||||
|
@ -258,4 +300,10 @@ class ApiClient {
|
|||
});
|
||||
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
28
lib/models/preset.dart
Normal 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;
|
||||
}
|
|
@ -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<AddMealScreen> {
|
||||
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
|
||||
void initState() {
|
||||
|
@ -21,6 +37,7 @@ class _AddMealScreen extends State<AddMealScreen> {
|
|||
setState(() {
|
||||
nameController.text = "Meal ${widget.diary.meals.length}";
|
||||
});
|
||||
_getPresets();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -53,26 +70,98 @@ class _AddMealScreen extends State<AddMealScreen> {
|
|||
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
|
||||
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: <Widget>[
|
||||
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),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -17,6 +17,69 @@ class MealWidget extends StatelessWidget {
|
|||
required this.apiClient,
|
||||
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
|
||||
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"),
|
||||
],
|
||||
),
|
||||
|
|
40
lib/widgets/preset.dart
Normal file
40
lib/widgets/preset.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue