[context] refactor screens etc to use global context storing client and storage, for easier further changes'
This commit is contained in:
parent
07c92443ce
commit
33fe0eff39
20 changed files with 521 additions and 488 deletions
351
lib/client.dart
351
lib/client.dart
|
@ -1,328 +1,35 @@
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:fooder/client/api.dart';
|
||||||
import 'dart:convert';
|
import 'package:fooder/client/product.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:fooder/client/entry.dart';
|
||||||
import 'package:fooder/models/meal.dart';
|
import 'package:fooder/client/preset.dart';
|
||||||
|
import 'package:fooder/client/meal.dart';
|
||||||
|
import 'package:fooder/client/diary.dart';
|
||||||
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
class Client {
|
||||||
|
final ApiClient api;
|
||||||
|
final ProductClient product;
|
||||||
|
final EntryClient entry;
|
||||||
|
final PresetClient preset;
|
||||||
|
final MealClient meal;
|
||||||
|
final DiaryClient diary;
|
||||||
|
|
||||||
class ApiClient {
|
Client(
|
||||||
final String baseUrl;
|
{required this.api,
|
||||||
String? token;
|
required this.product,
|
||||||
String? refreshToken;
|
required this.entry,
|
||||||
http.Client httpClient = http.Client();
|
required this.meal,
|
||||||
final FlutterSecureStorage storage = const FlutterSecureStorage();
|
required this.diary,
|
||||||
|
required this.preset});
|
||||||
|
|
||||||
ApiClient({
|
static Future<Client> create({required String baseUrl}) async {
|
||||||
required this.baseUrl,
|
var api = await ApiClient.create(baseUrl: baseUrl);
|
||||||
});
|
|
||||||
|
|
||||||
static Future<ApiClient> create({required String baseUrl}) async {
|
return Client(
|
||||||
var client = ApiClient(baseUrl: baseUrl);
|
api: api,
|
||||||
client.loadToken();
|
product: ProductClient(apiClient: api),
|
||||||
return client;
|
preset: PresetClient(apiClient: api),
|
||||||
}
|
meal: MealClient(apiClient: api),
|
||||||
|
diary: DiaryClient(apiClient: api),
|
||||||
Future<void> loadToken() async {
|
entry: EntryClient(apiClient: api));
|
||||||
Map<String, String> allValues = await storage.readAll();
|
|
||||||
|
|
||||||
if (allValues.containsKey('token')) {
|
|
||||||
token = allValues['token'];
|
|
||||||
}
|
|
||||||
if (allValues.containsKey('refreshToken')) {
|
|
||||||
refreshToken = allValues['refreshToken'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> headers({bool forGet = false, bool forLogin = false}) {
|
|
||||||
if (token == null && !forLogin) {
|
|
||||||
throw Exception('Not logged in');
|
|
||||||
}
|
|
||||||
|
|
||||||
final headers = {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!forGet) {
|
|
||||||
headers['Content-Type'] = 'application/json';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token != null) {
|
|
||||||
headers['Authorization'] = 'Bearer $token';
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> _jsonDecode(http.Response response) {
|
|
||||||
try {
|
|
||||||
return jsonDecode(utf8.decode(response.bodyBytes));
|
|
||||||
} catch (e) {
|
|
||||||
throw Exception('Response returned status code: ${response.statusCode}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> get(String path) async {
|
|
||||||
final response = await httpClient.get(
|
|
||||||
Uri.parse('$baseUrl$path'),
|
|
||||||
headers: headers(forGet: true),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 401) {
|
|
||||||
await refresh();
|
|
||||||
return await get(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw Exception('Response returned status code: ${response.statusCode}');
|
|
||||||
}
|
|
||||||
|
|
||||||
return _jsonDecode(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> post(String path, Map<String, dynamic> body,
|
|
||||||
{bool forLogin = 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 post(path, body, forLogin: forLogin);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw Exception('Response returned status code: ${response.statusCode}');
|
|
||||||
}
|
|
||||||
|
|
||||||
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'),
|
|
||||||
headers: headers(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 401) {
|
|
||||||
await refresh();
|
|
||||||
return await delete(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw Exception('Response returned status code: ${response.statusCode}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> patch(
|
|
||||||
String path, Map<String, dynamic> body) async {
|
|
||||||
final response = await httpClient.patch(
|
|
||||||
Uri.parse('$baseUrl$path'),
|
|
||||||
body: jsonEncode(body),
|
|
||||||
headers: headers(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 401) {
|
|
||||||
await refresh();
|
|
||||||
return await patch(path, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw Exception('Response returned status code: ${response.statusCode}');
|
|
||||||
}
|
|
||||||
|
|
||||||
return _jsonDecode(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> login(String username, String password) async {
|
|
||||||
final headers = {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
final response = await httpClient.post(
|
|
||||||
Uri.parse('$baseUrl/token'),
|
|
||||||
body: {
|
|
||||||
'username': username,
|
|
||||||
'password': password,
|
|
||||||
},
|
|
||||||
encoding: Encoding.getByName('utf-8'),
|
|
||||||
headers: headers,
|
|
||||||
);
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw Exception('Failed to login');
|
|
||||||
}
|
|
||||||
|
|
||||||
final token = _jsonDecode(response)['access_token'];
|
|
||||||
this.token = token;
|
|
||||||
await storage.write(key: 'token', value: token);
|
|
||||||
|
|
||||||
final refreshToken = _jsonDecode(response)['refresh_token'];
|
|
||||||
this.refreshToken = refreshToken;
|
|
||||||
await storage.write(key: 'refreshToken', value: refreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> refresh() async {
|
|
||||||
if (refreshToken == null) {
|
|
||||||
throw Exception("No valid refresh token found");
|
|
||||||
}
|
|
||||||
|
|
||||||
final response = await post("/token/refresh", {
|
|
||||||
"refresh_token": refreshToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
token = response['access_token'] as String;
|
|
||||||
await storage.write(key: 'token', value: token);
|
|
||||||
|
|
||||||
refreshToken = response['refresh_token'] as String;
|
|
||||||
await storage.write(key: 'refreshToken', value: refreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getDiary({required DateTime date}) async {
|
|
||||||
var formatter = DateFormat('yyyy-MM-dd');
|
|
||||||
var params = {
|
|
||||||
"date": formatter.format(date),
|
|
||||||
};
|
|
||||||
var response = await get("/diary?${Uri(queryParameters: params).query}");
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> logout() async {
|
|
||||||
token = null;
|
|
||||||
refreshToken = null;
|
|
||||||
|
|
||||||
await storage.deleteAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getProducts(String q) async {
|
|
||||||
var response =
|
|
||||||
await get("/product?${Uri(queryParameters: {"q": q}).query}");
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getProductByBarcode(String barcode) async {
|
|
||||||
var response = await get("/product/by_barcode?${Uri(queryParameters: {
|
|
||||||
"barcode": barcode
|
|
||||||
}).query}");
|
|
||||||
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,
|
|
||||||
required int mealId,
|
|
||||||
}) async {
|
|
||||||
var entry = {
|
|
||||||
"grams": grams,
|
|
||||||
"product_id": productId,
|
|
||||||
"meal_id": mealId,
|
|
||||||
};
|
|
||||||
await post("/entry", entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteEntry(int id) async {
|
|
||||||
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,
|
|
||||||
required int productId,
|
|
||||||
required int mealId,
|
|
||||||
}) async {
|
|
||||||
var entry = {
|
|
||||||
"grams": grams,
|
|
||||||
"product_id": productId,
|
|
||||||
"meal_id": mealId,
|
|
||||||
};
|
|
||||||
await patch("/entry/$id", entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> register(String username, String password) async {
|
|
||||||
try {
|
|
||||||
await post(
|
|
||||||
"/user",
|
|
||||||
{
|
|
||||||
"username": username,
|
|
||||||
"password": password,
|
|
||||||
},
|
|
||||||
forLogin: true,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
throw Exception("Failed to register");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addMeal({required String name, required int diaryId}) async {
|
|
||||||
await post("/meal", {
|
|
||||||
"name": name,
|
|
||||||
"diary_id": diaryId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addMealFromPreset(
|
|
||||||
{required String name,
|
|
||||||
required int diaryId,
|
|
||||||
required int presetId}) async {
|
|
||||||
await post("/meal/from_preset", {
|
|
||||||
"name": name,
|
|
||||||
"diary_id": diaryId,
|
|
||||||
"preset_id": presetId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> addProduct({
|
|
||||||
required String name,
|
|
||||||
required double protein,
|
|
||||||
required double carb,
|
|
||||||
required double fat,
|
|
||||||
required double fiber,
|
|
||||||
}) async {
|
|
||||||
var response = await post("/product", {
|
|
||||||
"name": name,
|
|
||||||
"protein": protein,
|
|
||||||
"carb": carb,
|
|
||||||
"fat": fat,
|
|
||||||
"fiber": fiber,
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveMeal(Meal meal, String name) async {
|
|
||||||
await postNoResult("/meal/${meal.id}/save", {
|
|
||||||
"name": name,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
219
lib/client/api.dart
Normal file
219
lib/client/api.dart
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
|
class ApiClient {
|
||||||
|
final String baseUrl;
|
||||||
|
String? token;
|
||||||
|
String? refreshToken;
|
||||||
|
http.Client httpClient = http.Client();
|
||||||
|
final FlutterSecureStorage storage = const FlutterSecureStorage();
|
||||||
|
|
||||||
|
ApiClient({
|
||||||
|
required this.baseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
static Future<ApiClient> create({required String baseUrl}) async {
|
||||||
|
var client = ApiClient(baseUrl: baseUrl);
|
||||||
|
client.loadToken();
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadToken() async {
|
||||||
|
Map<String, String> allValues = await storage.readAll();
|
||||||
|
|
||||||
|
if (allValues.containsKey('token')) {
|
||||||
|
token = allValues['token'];
|
||||||
|
}
|
||||||
|
if (allValues.containsKey('refreshToken')) {
|
||||||
|
refreshToken = allValues['refreshToken'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> headers({bool forGet = false, bool forLogin = false}) {
|
||||||
|
if (token == null && !forLogin) {
|
||||||
|
throw Exception('Not logged in');
|
||||||
|
}
|
||||||
|
|
||||||
|
final headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!forGet) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token != null) {
|
||||||
|
headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _jsonDecode(http.Response response) {
|
||||||
|
try {
|
||||||
|
return jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Response returned status code: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> get(String path) async {
|
||||||
|
final response = await httpClient.get(
|
||||||
|
Uri.parse('$baseUrl$path'),
|
||||||
|
headers: headers(forGet: true),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 401) {
|
||||||
|
await refresh();
|
||||||
|
return await get(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Response returned status code: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return _jsonDecode(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> post(String path, Map<String, dynamic> body,
|
||||||
|
{bool forLogin = 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 post(path, body, forLogin: forLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Response returned status code: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
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'),
|
||||||
|
headers: headers(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 401) {
|
||||||
|
await refresh();
|
||||||
|
return await delete(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Response returned status code: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> patch(
|
||||||
|
String path, Map<String, dynamic> body) async {
|
||||||
|
final response = await httpClient.patch(
|
||||||
|
Uri.parse('$baseUrl$path'),
|
||||||
|
body: jsonEncode(body),
|
||||||
|
headers: headers(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 401) {
|
||||||
|
await refresh();
|
||||||
|
return await patch(path, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Response returned status code: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return _jsonDecode(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> login(String username, String password) async {
|
||||||
|
final headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
final response = await httpClient.post(
|
||||||
|
Uri.parse('$baseUrl/token'),
|
||||||
|
body: {
|
||||||
|
'username': username,
|
||||||
|
'password': password,
|
||||||
|
},
|
||||||
|
encoding: Encoding.getByName('utf-8'),
|
||||||
|
headers: headers,
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Failed to login');
|
||||||
|
}
|
||||||
|
|
||||||
|
final token = _jsonDecode(response)['access_token'];
|
||||||
|
this.token = token;
|
||||||
|
await storage.write(key: 'token', value: token);
|
||||||
|
|
||||||
|
final refreshToken = _jsonDecode(response)['refresh_token'];
|
||||||
|
this.refreshToken = refreshToken;
|
||||||
|
await storage.write(key: 'refreshToken', value: refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
if (refreshToken == null) {
|
||||||
|
throw Exception("No valid refresh token found");
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await post("/token/refresh", {
|
||||||
|
"refresh_token": refreshToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
token = response['access_token'] as String;
|
||||||
|
await storage.write(key: 'token', value: token);
|
||||||
|
|
||||||
|
refreshToken = response['refresh_token'] as String;
|
||||||
|
await storage.write(key: 'refreshToken', value: refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> logout() async {
|
||||||
|
token = null;
|
||||||
|
refreshToken = null;
|
||||||
|
|
||||||
|
await storage.deleteAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> register(String username, String password) async {
|
||||||
|
try {
|
||||||
|
await post(
|
||||||
|
"/user",
|
||||||
|
{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
},
|
||||||
|
forLogin: true,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception("Failed to register");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1,7 @@
|
||||||
import 'package:fooder/client.dart';
|
import 'package:fooder/client/api.dart';
|
||||||
|
|
||||||
|
abstract class BasedClient {
|
||||||
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
const BasedClient({required this.apiClient});
|
||||||
|
}
|
||||||
|
|
18
lib/client/diary.dart
Normal file
18
lib/client/diary.dart
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import 'package:fooder/client/based.dart';
|
||||||
|
import 'package:fooder/models/diary.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class DiaryClient extends BasedClient {
|
||||||
|
const DiaryClient({required super.apiClient});
|
||||||
|
|
||||||
|
Future<Diary> get({required DateTime date}) async {
|
||||||
|
var formatter = DateFormat('yyyy-MM-dd');
|
||||||
|
var params = {
|
||||||
|
"date": formatter.format(date),
|
||||||
|
};
|
||||||
|
var response =
|
||||||
|
await apiClient.get("/diary?${Uri(queryParameters: params).query}");
|
||||||
|
|
||||||
|
return Diary.fromJson(response);
|
||||||
|
}
|
||||||
|
}
|
36
lib/client/entry.dart
Normal file
36
lib/client/entry.dart
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import 'package:fooder/client/based.dart';
|
||||||
|
|
||||||
|
class EntryClient extends BasedClient {
|
||||||
|
const EntryClient({required super.apiClient});
|
||||||
|
|
||||||
|
Future<void> create({
|
||||||
|
required double grams,
|
||||||
|
required int productId,
|
||||||
|
required int mealId,
|
||||||
|
}) async {
|
||||||
|
var entry = {
|
||||||
|
"grams": grams,
|
||||||
|
"product_id": productId,
|
||||||
|
"meal_id": mealId,
|
||||||
|
};
|
||||||
|
await apiClient.post("/entry", entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> delete(int id) async {
|
||||||
|
await apiClient.delete("/entry/$id");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> update(
|
||||||
|
int id, {
|
||||||
|
required double grams,
|
||||||
|
required int productId,
|
||||||
|
required int mealId,
|
||||||
|
}) async {
|
||||||
|
var entry = {
|
||||||
|
"grams": grams,
|
||||||
|
"product_id": productId,
|
||||||
|
"meal_id": mealId,
|
||||||
|
};
|
||||||
|
await apiClient.patch("/entry/$id", entry);
|
||||||
|
}
|
||||||
|
}
|
33
lib/client/meal.dart
Normal file
33
lib/client/meal.dart
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import 'package:fooder/client/based.dart';
|
||||||
|
|
||||||
|
class MealClient extends BasedClient {
|
||||||
|
const MealClient({required super.apiClient});
|
||||||
|
|
||||||
|
Future<void> create({required String name, required int diaryId}) async {
|
||||||
|
await apiClient.post("/meal", {
|
||||||
|
"name": name,
|
||||||
|
"diary_id": diaryId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createFromPreset(
|
||||||
|
{required String name,
|
||||||
|
required int diaryId,
|
||||||
|
required int presetId}) async {
|
||||||
|
await apiClient.post("/meal/from_preset", {
|
||||||
|
"name": name,
|
||||||
|
"diary_id": diaryId,
|
||||||
|
"preset_id": presetId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> update(int id, String name) async {
|
||||||
|
await apiClient.postNoResult("/meal/$id/save", {
|
||||||
|
"name": name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> delete(int id) async {
|
||||||
|
await apiClient.delete("/meal/$id");
|
||||||
|
}
|
||||||
|
}
|
18
lib/client/preset.dart
Normal file
18
lib/client/preset.dart
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import 'package:fooder/client/based.dart';
|
||||||
|
import 'package:fooder/models/preset.dart';
|
||||||
|
|
||||||
|
class PresetClient extends BasedClient {
|
||||||
|
const PresetClient({required super.apiClient});
|
||||||
|
|
||||||
|
Future<List<Preset>> list(String? q) async {
|
||||||
|
var response =
|
||||||
|
await apiClient.get("/preset?${Uri(queryParameters: {"q": q}).query}");
|
||||||
|
return (response['presets'] as List<dynamic>)
|
||||||
|
.map((e) => Preset.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> delete(int id) async {
|
||||||
|
await apiClient.delete("/preset/$id");
|
||||||
|
}
|
||||||
|
}
|
41
lib/client/product.dart
Normal file
41
lib/client/product.dart
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import 'package:fooder/client/based.dart';
|
||||||
|
import 'package:fooder/models/product.dart';
|
||||||
|
|
||||||
|
class ProductClient extends BasedClient {
|
||||||
|
const ProductClient({required super.apiClient});
|
||||||
|
|
||||||
|
Future<List<Product>> list(String q) async {
|
||||||
|
var response =
|
||||||
|
await apiClient.get("/product?${Uri(queryParameters: {"q": q}).query}");
|
||||||
|
|
||||||
|
return (response['products'] as List<dynamic>)
|
||||||
|
.map((e) => Product.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Product> getByBarcode(String barcode) async {
|
||||||
|
var response = await apiClient.get(
|
||||||
|
"/product/by_barcode?${Uri(queryParameters: {
|
||||||
|
"barcode": barcode
|
||||||
|
}).query}");
|
||||||
|
|
||||||
|
return Product.fromJson(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Product> create({
|
||||||
|
required String name,
|
||||||
|
required double protein,
|
||||||
|
required double carb,
|
||||||
|
required double fat,
|
||||||
|
required double fiber,
|
||||||
|
}) async {
|
||||||
|
var response = await apiClient.post("/product", {
|
||||||
|
"name": name,
|
||||||
|
"protein": protein,
|
||||||
|
"carb": carb,
|
||||||
|
"fat": fat,
|
||||||
|
"fiber": fiber,
|
||||||
|
});
|
||||||
|
return Product.fromJson(response);
|
||||||
|
}
|
||||||
|
}
|
16
lib/context.dart
Normal file
16
lib/context.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:fooder/client.dart';
|
||||||
|
import 'package:fooder/storage.dart';
|
||||||
|
|
||||||
|
class Context {
|
||||||
|
final Client client;
|
||||||
|
final Storage storage;
|
||||||
|
|
||||||
|
Context({required this.client, required this.storage});
|
||||||
|
|
||||||
|
static Future<Context> create({required String baseUrl}) async {
|
||||||
|
var client = await Client.create(baseUrl: baseUrl);
|
||||||
|
var storage = await Storage.create();
|
||||||
|
|
||||||
|
return Context(client: client, storage: storage);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,12 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fooder/screens/login.dart';
|
import 'package:fooder/screens/login.dart';
|
||||||
import 'package:fooder/client.dart';
|
import 'package:fooder/context.dart';
|
||||||
import 'package:fooder/storage.dart';
|
|
||||||
import 'package:fooder/theme.dart';
|
import 'package:fooder/theme.dart';
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
final Storage storage;
|
final Context ctx;
|
||||||
final ApiClient apiClient;
|
|
||||||
|
|
||||||
const MyApp({required this.storage, required this.apiClient, super.key});
|
const MyApp({required this.ctx, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -19,18 +17,16 @@ class MyApp extends StatelessWidget {
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.system,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
home: LoginScreen(
|
home: LoginScreen(
|
||||||
apiClient: apiClient,
|
ctx: ctx,
|
||||||
storage: storage,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
var storage = await Storage.create();
|
var ctx = await Context.create(
|
||||||
var apiClient = await ApiClient.create(
|
|
||||||
baseUrl: 'https://fooderapi.domandoman.xyz/api',
|
baseUrl: 'https://fooderapi.domandoman.xyz/api',
|
||||||
);
|
);
|
||||||
|
|
||||||
runApp(MyApp(storage: storage, apiClient: apiClient));
|
runApp(MyApp(ctx: ctx));
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,7 @@ import 'package:simple_barcode_scanner/simple_barcode_scanner.dart';
|
||||||
class AddEntryScreen extends BasedScreen {
|
class AddEntryScreen extends BasedScreen {
|
||||||
final Diary diary;
|
final Diary diary;
|
||||||
|
|
||||||
const AddEntryScreen(
|
const AddEntryScreen({super.key, required super.ctx, required this.diary});
|
||||||
{super.key,
|
|
||||||
required super.apiClient,
|
|
||||||
required super.storage,
|
|
||||||
required this.diary});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AddEntryScreen> createState() => _AddEntryScreen();
|
State<AddEntryScreen> createState() => _AddEntryScreen();
|
||||||
|
@ -54,14 +50,10 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getProducts() async {
|
Future<void> _getProducts() async {
|
||||||
var productsMap = await apiClient.getProducts(productNameController.text);
|
var products = await client.product.list(productNameController.text);
|
||||||
|
|
||||||
var parsedProducts = (productsMap['products'] as List<dynamic>)
|
|
||||||
.map((e) => Product.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
products = parsedProducts;
|
this.products = products;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +91,7 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await apiClient.addEntry(
|
await client.entry.create(
|
||||||
grams: grams,
|
grams: grams,
|
||||||
productId: products[0].id,
|
productId: products[0].id,
|
||||||
mealId: meal!.id,
|
mealId: meal!.id,
|
||||||
|
@ -117,9 +109,7 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
|
||||||
|
|
||||||
if (res is String) {
|
if (res is String) {
|
||||||
try {
|
try {
|
||||||
var productMap = await apiClient.getProductByBarcode(res);
|
var product = await client.product.getByBarcode(res);
|
||||||
|
|
||||||
var product = Product.fromJson(productMap);
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
products = [product];
|
products = [product];
|
||||||
|
@ -185,8 +175,7 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => AddProductScreen(
|
builder: (context) => AddProductScreen(
|
||||||
apiClient: apiClient,
|
ctx: ctx,
|
||||||
storage: storage,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((product) {
|
).then((product) {
|
||||||
|
|
|
@ -9,11 +9,7 @@ import 'package:fooder/components/floating_action_button.dart';
|
||||||
class AddMealScreen extends BasedScreen {
|
class AddMealScreen extends BasedScreen {
|
||||||
final Diary diary;
|
final Diary diary;
|
||||||
|
|
||||||
const AddMealScreen(
|
const AddMealScreen({super.key, required super.ctx, required this.diary});
|
||||||
{super.key,
|
|
||||||
required super.apiClient,
|
|
||||||
required super.storage,
|
|
||||||
required this.diary});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AddMealScreen> createState() => _AddMealScreen();
|
State<AddMealScreen> createState() => _AddMealScreen();
|
||||||
|
@ -28,12 +24,10 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
|
||||||
Diary get diary => widget.diary;
|
Diary get diary => widget.diary;
|
||||||
|
|
||||||
Future<void> _getPresets() async {
|
Future<void> _getPresets() async {
|
||||||
var presetsMap = await apiClient.getPresets(presetNameController.text);
|
var presets = await client.preset.list(presetNameController.text);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
presets = (presetsMap['presets'] as List<dynamic>)
|
this.presets = presets;
|
||||||
.map((e) => Preset.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList();
|
|
||||||
selectedPreset = null;
|
selectedPreset = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -59,22 +53,22 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addMeal() async {
|
Future<void> addMeal() async {
|
||||||
await apiClient.addMeal(
|
await client.meal.create(
|
||||||
name: nameController.text,
|
name: nameController.text,
|
||||||
diaryId: diary.id,
|
diaryId: diary.id,
|
||||||
);
|
);
|
||||||
popMeDaddy();
|
popMeDaddy();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deletePreset(Preset preset) async {
|
Future<void> deletePreset(Preset preset) async {
|
||||||
apiClient.deletePreset(preset.id);
|
client.preset.delete(preset.id);
|
||||||
setState(() {
|
setState(() {
|
||||||
presets.remove(preset);
|
presets.remove(preset);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deletePreset(context, Preset preset) async {
|
Future<void> deletePresetPopup(context, Preset preset) async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
|
@ -90,7 +84,7 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_deletePreset(preset);
|
deletePreset(preset);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -100,13 +94,13 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addMealFromPreset() async {
|
Future<void> addMealFromPreset() async {
|
||||||
if (selectedPreset == null) {
|
if (selectedPreset == null) {
|
||||||
_addMeal();
|
addMeal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await apiClient.addMealFromPreset(
|
await client.meal.createFromPreset(
|
||||||
name: nameChanged ? nameController.text : selectedPreset!.name,
|
name: nameChanged ? nameController.text : selectedPreset!.name,
|
||||||
diaryId: diary.id,
|
diaryId: diary.id,
|
||||||
presetId: selectedPreset!.id,
|
presetId: selectedPreset!.id,
|
||||||
|
@ -143,10 +137,10 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
|
||||||
presetNameController.text = preset.name;
|
presetNameController.text = preset.name;
|
||||||
selectedPreset = preset;
|
selectedPreset = preset;
|
||||||
});
|
});
|
||||||
_addMealFromPreset();
|
addMealFromPreset();
|
||||||
},
|
},
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
deletePreset(context, preset);
|
deletePresetPopup(context, preset);
|
||||||
},
|
},
|
||||||
title: PresetWidget(
|
title: PresetWidget(
|
||||||
preset: preset,
|
preset: preset,
|
||||||
|
@ -156,7 +150,7 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: FActionButton(
|
floatingActionButton: FActionButton(
|
||||||
onPressed: _addMealFromPreset,
|
onPressed: addMealFromPreset,
|
||||||
icon: Icons.playlist_add_rounded,
|
icon: Icons.playlist_add_rounded,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,8 +7,7 @@ import 'package:fooder/components/text.dart';
|
||||||
import 'package:fooder/components/floating_action_button.dart';
|
import 'package:fooder/components/floating_action_button.dart';
|
||||||
|
|
||||||
class AddProductScreen extends BasedScreen {
|
class AddProductScreen extends BasedScreen {
|
||||||
const AddProductScreen(
|
const AddProductScreen({super.key, required super.ctx});
|
||||||
{super.key, required super.apiClient, required super.storage});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AddProductScreen> createState() => _AddProductScreen();
|
State<AddProductScreen> createState() => _AddProductScreen();
|
||||||
|
@ -38,38 +37,25 @@ class _AddProductScreen extends BasedState<AddProductScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<double?> _parseDouble(String text, String name,
|
Future<void> addProduct() async {
|
||||||
{bool silent = false}) async {
|
var carb = await parseDouble(carbController.text, "Carbs");
|
||||||
try {
|
var fat = await parseDouble(fatController.text, "Fat");
|
||||||
return double.parse(text.replaceAll(",", "."));
|
var protein = await parseDouble(proteinController.text, "Protein");
|
||||||
} catch (e) {
|
|
||||||
if (!silent) {
|
|
||||||
showError("$name must be a number");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _addProduct() async {
|
|
||||||
var carb = await _parseDouble(carbController.text, "Carbs");
|
|
||||||
var fat = await _parseDouble(fatController.text, "Fat");
|
|
||||||
var protein = await _parseDouble(proteinController.text, "Protein");
|
|
||||||
var fiber =
|
var fiber =
|
||||||
await _parseDouble(fiberController.text, "Fiber", silent: true) ?? 0;
|
await parseDouble(fiberController.text, "Fiber", silent: true) ?? 0;
|
||||||
|
|
||||||
if (carb == null || fat == null || protein == null) {
|
if (carb == null || fat == null || protein == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var productJson = await apiClient.addProduct(
|
var product = await client.product.create(
|
||||||
carb: carb,
|
carb: carb,
|
||||||
fat: fat,
|
fat: fat,
|
||||||
protein: protein,
|
protein: protein,
|
||||||
fiber: fiber,
|
fiber: fiber,
|
||||||
name: nameController.text,
|
name: nameController.text,
|
||||||
);
|
);
|
||||||
var product = Product.fromJson(productJson);
|
|
||||||
popMeDaddy(product);
|
popMeDaddy(product);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(
|
showError(
|
||||||
|
@ -195,7 +181,7 @@ class _AddProductScreen extends BasedState<AddProductScreen> {
|
||||||
])),
|
])),
|
||||||
),
|
),
|
||||||
floatingActionButton: FActionButton(
|
floatingActionButton: FActionButton(
|
||||||
onPressed: _addProduct,
|
onPressed: addProduct,
|
||||||
icon: Icons.save,
|
icon: Icons.save,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fooder/context.dart';
|
||||||
import 'package:fooder/client.dart';
|
import 'package:fooder/client.dart';
|
||||||
import 'package:fooder/storage.dart';
|
import 'package:fooder/storage.dart';
|
||||||
import 'package:fooder/components/app_bar.dart';
|
import 'package:fooder/components/app_bar.dart';
|
||||||
|
@ -13,19 +14,18 @@ TextStyle logoStyle(context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class BasedScreen extends StatefulWidget {
|
abstract class BasedScreen extends StatefulWidget {
|
||||||
final ApiClient apiClient;
|
final Context ctx;
|
||||||
final Storage storage;
|
|
||||||
|
|
||||||
const BasedScreen(
|
const BasedScreen({super.key, required this.ctx});
|
||||||
{super.key, required this.apiClient, required this.storage});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class BasedState<T extends BasedScreen> extends State<T> {
|
abstract class BasedState<T extends BasedScreen> extends State<T> {
|
||||||
ApiClient get apiClient => widget.apiClient;
|
Context get ctx => widget.ctx;
|
||||||
Storage get storage => widget.storage;
|
Client get client => widget.ctx.client;
|
||||||
|
Storage get storage => widget.ctx.storage;
|
||||||
|
|
||||||
void logout() async {
|
void logout() async {
|
||||||
await apiClient.logout();
|
await client.api.logout();
|
||||||
backToLogin();
|
backToLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,8 +33,7 @@ abstract class BasedState<T extends BasedScreen> extends State<T> {
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) => LoginScreen(ctx: ctx),
|
||||||
LoginScreen(apiClient: apiClient, storage: storage),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -43,8 +42,7 @@ abstract class BasedState<T extends BasedScreen> extends State<T> {
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) => MainScreen(ctx: ctx),
|
||||||
MainScreen(apiClient: apiClient, storage: storage),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -103,6 +101,18 @@ abstract class BasedState<T extends BasedScreen> extends State<T> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<double?> parseDouble(String text, String name,
|
||||||
|
{bool silent = false}) async {
|
||||||
|
try {
|
||||||
|
return double.parse(text.replaceAll(",", "."));
|
||||||
|
} catch (e) {
|
||||||
|
if (!silent) {
|
||||||
|
showError("$name must be a number");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void showError(String message) {
|
void showError(String message) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|
|
@ -12,11 +12,7 @@ import 'package:simple_barcode_scanner/simple_barcode_scanner.dart';
|
||||||
class EditEntryScreen extends BasedScreen {
|
class EditEntryScreen extends BasedScreen {
|
||||||
final Entry entry;
|
final Entry entry;
|
||||||
|
|
||||||
const EditEntryScreen(
|
const EditEntryScreen({super.key, required super.ctx, required this.entry});
|
||||||
{super.key,
|
|
||||||
required super.apiClient,
|
|
||||||
required super.storage,
|
|
||||||
required this.entry});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<EditEntryScreen> createState() => _EditEntryScreen();
|
State<EditEntryScreen> createState() => _EditEntryScreen();
|
||||||
|
@ -26,7 +22,7 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
|
||||||
final gramsController = TextEditingController();
|
final gramsController = TextEditingController();
|
||||||
final productNameController = TextEditingController();
|
final productNameController = TextEditingController();
|
||||||
List<Product> products = [];
|
List<Product> products = [];
|
||||||
Entry get entry => entry;
|
Entry get entry => widget.entry;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
@ -49,36 +45,25 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getProducts() async {
|
Future<void> getProducts() async {
|
||||||
var productsMap = await apiClient.getProducts(productNameController.text);
|
var products = await client.product.list(productNameController.text);
|
||||||
setState(() {
|
setState(() {
|
||||||
products = (productsMap['products'] as List<dynamic>)
|
this.products = products;
|
||||||
.map((e) => Product.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<double?> _parseDouble(String text, String name) async {
|
Future<void> saveEntry() async {
|
||||||
try {
|
|
||||||
return double.parse(text.replaceAll(",", "."));
|
|
||||||
} catch (e) {
|
|
||||||
showError("$name must be a number");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _saveEntry() async {
|
|
||||||
if (products.length != 1) {
|
if (products.length != 1) {
|
||||||
showError("Pick product first");
|
showError("Pick product first");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var grams = await _parseDouble(gramsController.text, "Grams");
|
var grams = await parseDouble(gramsController.text, "Grams");
|
||||||
if (grams == null) {
|
if (grams == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await apiClient.updateEntry(
|
await client.entry.update(
|
||||||
entry.id,
|
entry.id,
|
||||||
grams: grams,
|
grams: grams,
|
||||||
productId: products[0].id,
|
productId: products[0].id,
|
||||||
|
@ -87,12 +72,12 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
|
||||||
popMeDaddy();
|
popMeDaddy();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteEntry() async {
|
Future<void> deleteEntry() async {
|
||||||
await apiClient.deleteEntry(widget.entry.id);
|
await client.entry.delete(widget.entry.id);
|
||||||
popMeDaddy();
|
popMeDaddy();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _findProductByBarCode() async {
|
Future<void> findProductByBarCode() async {
|
||||||
var res = await Navigator.push(
|
var res = await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
@ -102,9 +87,7 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
|
||||||
|
|
||||||
if (res is String) {
|
if (res is String) {
|
||||||
try {
|
try {
|
||||||
var productMap = await apiClient.getProductByBarcode(res);
|
var product = await client.product.getByBarcode(res);
|
||||||
|
|
||||||
var product = Product.fromJson(productMap);
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
products = [product];
|
products = [product];
|
||||||
|
@ -128,7 +111,7 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
|
||||||
FTextInput(
|
FTextInput(
|
||||||
labelText: 'Product name',
|
labelText: 'Product name',
|
||||||
controller: productNameController,
|
controller: productNameController,
|
||||||
onChanged: (_) => _getProducts(),
|
onChanged: (_) => getProducts(),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
),
|
),
|
||||||
FTextInput(
|
FTextInput(
|
||||||
|
@ -147,8 +130,7 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => AddProductScreen(
|
builder: (context) => AddProductScreen(
|
||||||
apiClient: apiClient,
|
ctx: ctx,
|
||||||
storage: storage,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((product) {
|
).then((product) {
|
||||||
|
@ -181,18 +163,18 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
FActionButton(
|
FActionButton(
|
||||||
onPressed: _findProductByBarCode,
|
onPressed: findProductByBarCode,
|
||||||
icon: Icons.photo_camera,
|
icon: Icons.photo_camera,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
FActionButton(
|
FActionButton(
|
||||||
onPressed: _deleteEntry,
|
onPressed: deleteEntry,
|
||||||
tag: "fap1",
|
tag: "fap1",
|
||||||
icon: Icons.delete,
|
icon: Icons.delete,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
FActionButton(
|
FActionButton(
|
||||||
onPressed: _saveEntry,
|
onPressed: saveEntry,
|
||||||
tag: "fap2",
|
tag: "fap2",
|
||||||
icon: Icons.save,
|
icon: Icons.save,
|
||||||
),
|
),
|
||||||
|
|
|
@ -7,8 +7,7 @@ import 'package:fooder/components/text.dart';
|
||||||
import 'package:fooder/components/button.dart';
|
import 'package:fooder/components/button.dart';
|
||||||
|
|
||||||
class LoginScreen extends BasedScreen {
|
class LoginScreen extends BasedScreen {
|
||||||
const LoginScreen(
|
const LoginScreen({super.key, required super.ctx});
|
||||||
{super.key, required super.apiClient, required super.storage});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LoginScreen> createState() => _LoginScreen();
|
State<LoginScreen> createState() => _LoginScreen();
|
||||||
|
@ -29,16 +28,15 @@ class _LoginScreen extends BasedState<LoginScreen> {
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) => MainScreen(ctx: ctx),
|
||||||
MainScreen(apiClient: apiClient, storage: storage),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// login client when button pressed
|
// login client when button pressed
|
||||||
Future<void> _login() async {
|
Future<void> login() async {
|
||||||
try {
|
try {
|
||||||
await apiClient.login(
|
await client.api.login(
|
||||||
usernameController.text,
|
usernameController.text,
|
||||||
passwordController.text,
|
passwordController.text,
|
||||||
);
|
);
|
||||||
|
@ -60,18 +58,18 @@ class _LoginScreen extends BasedState<LoginScreen> {
|
||||||
AutofillHints.password,
|
AutofillHints.password,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
_asyncInitState().then((value) => null);
|
asyncInitState().then((value) => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _asyncInitState() async {
|
Future<void> asyncInitState() async {
|
||||||
await apiClient.loadToken();
|
await client.api.loadToken();
|
||||||
|
|
||||||
if (apiClient.refreshToken == null) {
|
if (client.api.refreshToken == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.refresh();
|
await client.api.refresh();
|
||||||
showText("Welcome back!");
|
showText("Welcome back!");
|
||||||
popMeDaddy();
|
popMeDaddy();
|
||||||
} on Exception catch (_) {
|
} on Exception catch (_) {
|
||||||
|
@ -107,13 +105,13 @@ class _LoginScreen extends BasedState<LoginScreen> {
|
||||||
FTextInput(
|
FTextInput(
|
||||||
labelText: 'Password',
|
labelText: 'Password',
|
||||||
controller: passwordController,
|
controller: passwordController,
|
||||||
onFieldSubmitted: (_) => _login(),
|
onFieldSubmitted: (_) => login(),
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
),
|
),
|
||||||
FButton(
|
FButton(
|
||||||
labelText: 'Sign In',
|
labelText: 'Sign In',
|
||||||
onPressed: _login,
|
onPressed: login,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
@ -123,8 +121,7 @@ class _LoginScreen extends BasedState<LoginScreen> {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => RegisterScreen(
|
builder: (context) => RegisterScreen(
|
||||||
apiClient: apiClient,
|
ctx: ctx,
|
||||||
storage: storage,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,8 +10,7 @@ import 'package:fooder/components/date_picker.dart';
|
||||||
import 'package:fooder/components/floating_action_button.dart';
|
import 'package:fooder/components/floating_action_button.dart';
|
||||||
|
|
||||||
class MainScreen extends BasedScreen {
|
class MainScreen extends BasedScreen {
|
||||||
const MainScreen(
|
const MainScreen({super.key, required super.ctx});
|
||||||
{super.key, required super.apiClient, required super.storage});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MainScreen> createState() => _MainScreen();
|
State<MainScreen> createState() => _MainScreen();
|
||||||
|
@ -28,10 +27,10 @@ class _MainScreen extends BasedState<MainScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _asyncInitState() async {
|
Future<void> _asyncInitState() async {
|
||||||
var diaryMap = await apiClient.getDiary(date: date);
|
var diary = await client.diary.get(date: date);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
diary = Diary.fromJson(diaryMap);
|
this.diary = diary;
|
||||||
date = date;
|
date = date;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -52,8 +51,7 @@ class _MainScreen extends BasedState<MainScreen> {
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => AddEntryScreen(
|
builder: (context) => AddEntryScreen(ctx: ctx, diary: diary!),
|
||||||
apiClient: apiClient, diary: diary!, storage: storage),
|
|
||||||
),
|
),
|
||||||
).then((_) => _asyncInitState());
|
).then((_) => _asyncInitState());
|
||||||
}
|
}
|
||||||
|
@ -67,9 +65,8 @@ class _MainScreen extends BasedState<MainScreen> {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => AddMealScreen(
|
builder: (context) => AddMealScreen(
|
||||||
apiClient: apiClient,
|
ctx: ctx,
|
||||||
diary: diary!,
|
diary: diary!,
|
||||||
storage: storage,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((_) => _asyncInitState());
|
).then((_) => _asyncInitState());
|
||||||
|
@ -91,13 +88,11 @@ class _MainScreen extends BasedState<MainScreen> {
|
||||||
[
|
[
|
||||||
SummaryWidget(
|
SummaryWidget(
|
||||||
diary: diary!,
|
diary: diary!,
|
||||||
apiClient: apiClient,
|
|
||||||
refreshParent: _asyncInitState,
|
refreshParent: _asyncInitState,
|
||||||
),
|
),
|
||||||
for (var (i, meal) in diary!.meals.indexed)
|
for (var (i, meal) in diary!.meals.indexed)
|
||||||
MealWidget(
|
MealWidget(
|
||||||
apiClient: apiClient,
|
ctx: ctx,
|
||||||
storage: storage,
|
|
||||||
meal: meal,
|
meal: meal,
|
||||||
refreshParent: _asyncInitState,
|
refreshParent: _asyncInitState,
|
||||||
initiallyExpanded: i == 0,
|
initiallyExpanded: i == 0,
|
||||||
|
|
|
@ -5,8 +5,7 @@ import 'package:fooder/components/text.dart';
|
||||||
import 'package:fooder/components/button.dart';
|
import 'package:fooder/components/button.dart';
|
||||||
|
|
||||||
class RegisterScreen extends BasedScreen {
|
class RegisterScreen extends BasedScreen {
|
||||||
const RegisterScreen(
|
const RegisterScreen({super.key, required super.ctx});
|
||||||
{super.key, required super.apiClient, required super.storage});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RegisterScreen> createState() => _RegisterScreen();
|
State<RegisterScreen> createState() => _RegisterScreen();
|
||||||
|
@ -50,7 +49,7 @@ class _RegisterScreen extends BasedState<RegisterScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.register(
|
await client.api.register(
|
||||||
usernameController.text,
|
usernameController.text,
|
||||||
passwordController.text,
|
passwordController.text,
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,8 +3,7 @@ import 'package:fooder/models/meal.dart';
|
||||||
import 'package:fooder/widgets/entry.dart';
|
import 'package:fooder/widgets/entry.dart';
|
||||||
import 'package:fooder/widgets/macro.dart';
|
import 'package:fooder/widgets/macro.dart';
|
||||||
import 'package:fooder/screens/edit_entry.dart';
|
import 'package:fooder/screens/edit_entry.dart';
|
||||||
import 'package:fooder/client.dart';
|
import 'package:fooder/context.dart';
|
||||||
import 'package:fooder/storage.dart';
|
|
||||||
import 'dart:core';
|
import 'dart:core';
|
||||||
|
|
||||||
class MealHeader extends StatelessWidget {
|
class MealHeader extends StatelessWidget {
|
||||||
|
@ -38,8 +37,7 @@ class MealWidget extends StatelessWidget {
|
||||||
static const maxWidth = 920.0;
|
static const maxWidth = 920.0;
|
||||||
|
|
||||||
final Meal meal;
|
final Meal meal;
|
||||||
final ApiClient apiClient;
|
final Context ctx;
|
||||||
final Storage storage;
|
|
||||||
final Function() refreshParent;
|
final Function() refreshParent;
|
||||||
final Function(String) showText;
|
final Function(String) showText;
|
||||||
final bool initiallyExpanded;
|
final bool initiallyExpanded;
|
||||||
|
@ -47,11 +45,10 @@ class MealWidget extends StatelessWidget {
|
||||||
const MealWidget({
|
const MealWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.meal,
|
required this.meal,
|
||||||
required this.apiClient,
|
required this.ctx,
|
||||||
required this.refreshParent,
|
required this.refreshParent,
|
||||||
required this.initiallyExpanded,
|
required this.initiallyExpanded,
|
||||||
required this.showText,
|
required this.showText,
|
||||||
required this.storage,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> saveMeal(context) async {
|
Future<void> saveMeal(context) async {
|
||||||
|
@ -77,7 +74,7 @@ class MealWidget extends StatelessWidget {
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.save),
|
icon: const Icon(Icons.save),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
apiClient.saveMeal(meal, textFieldController.text);
|
ctx.client.meal.update(meal.id, textFieldController.text);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
showText("Meal saved");
|
showText("Meal saved");
|
||||||
},
|
},
|
||||||
|
@ -88,11 +85,11 @@ class MealWidget extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteMeal(Meal meal) async {
|
Future<void> deleteMeal(Meal meal) async {
|
||||||
await apiClient.deleteMeal(meal.id);
|
await ctx.client.meal.delete(meal.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteMeal(context) async {
|
Future<void> deleteMealPopup(context) async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
|
@ -109,7 +106,7 @@ class MealWidget extends StatelessWidget {
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_deleteMeal(meal).then((_) => refreshParent());
|
deleteMeal(meal).then((_) => refreshParent());
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
showText("Meal deleted");
|
showText("Meal deleted");
|
||||||
},
|
},
|
||||||
|
@ -125,9 +122,8 @@ class MealWidget extends StatelessWidget {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => EditEntryScreen(
|
builder: (context) => EditEntryScreen(
|
||||||
apiClient: apiClient,
|
ctx: ctx,
|
||||||
entry: entry,
|
entry: entry,
|
||||||
storage: storage,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((_) => refreshParent());
|
).then((_) => refreshParent());
|
||||||
|
@ -195,7 +191,7 @@ class MealWidget extends StatelessWidget {
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
onPressed: () => deleteMeal(context),
|
onPressed: () => deleteMealPopup(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fooder/models/diary.dart';
|
import 'package:fooder/models/diary.dart';
|
||||||
import 'package:fooder/widgets/macro.dart';
|
import 'package:fooder/widgets/macro.dart';
|
||||||
import 'package:fooder/client.dart';
|
|
||||||
import 'dart:core';
|
import 'dart:core';
|
||||||
|
|
||||||
class SummaryHeader extends StatelessWidget {
|
class SummaryHeader extends StatelessWidget {
|
||||||
|
@ -33,14 +32,10 @@ class SummaryWidget extends StatelessWidget {
|
||||||
static const maxWidth = 920.0;
|
static const maxWidth = 920.0;
|
||||||
|
|
||||||
final Diary diary;
|
final Diary diary;
|
||||||
final ApiClient apiClient;
|
|
||||||
final Function() refreshParent;
|
final Function() refreshParent;
|
||||||
|
|
||||||
const SummaryWidget(
|
const SummaryWidget(
|
||||||
{super.key,
|
{super.key, required this.diary, required this.refreshParent});
|
||||||
required this.diary,
|
|
||||||
required this.apiClient,
|
|
||||||
required this.refreshParent});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
Loading…
Reference in a new issue