[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,8 +70,51 @@ 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,
 | 
			
		||||
| 
						 | 
				
			
			@ -64,15 +124,44 @@ class _AddMealScreen extends State<AddMealScreen> {
 | 
			
		|||
        child: Container(
 | 
			
		||||
          constraints: const BoxConstraints(maxWidth: 720),
 | 
			
		||||
          padding: const EdgeInsets.all(10),
 | 
			
		||||
        child: TextFormField(
 | 
			
		||||
          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