Compare commits
5 commits
main
...
local_stor
Author | SHA1 | Date | |
---|---|---|---|
94042eb213 | |||
85cdd68ac1 | |||
33fe0eff39 | |||
07c92443ce | |||
1100114040 |
37 changed files with 959 additions and 515 deletions
|
@ -9,6 +9,9 @@ PODS:
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- sqflite (0.0.3):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
|
@ -16,6 +19,7 @@ DEPENDENCIES:
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
|
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
Flutter:
|
Flutter:
|
||||||
|
@ -28,6 +32,8 @@ EXTERNAL SOURCES:
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
|
sqflite:
|
||||||
|
:path: ".symlinks/plugins/sqflite/darwin"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
|
@ -35,6 +41,7 @@ SPEC CHECKSUMS:
|
||||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||||
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
|
|
||||||
PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
|
PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
|
||||||
|
|
||||||
|
|
349
lib/client.dart
349
lib/client.dart
|
@ -1,326 +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);
|
||||||
}) {
|
|
||||||
() async {
|
|
||||||
await loadToken();
|
|
||||||
}();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadToken() async {
|
return Client(
|
||||||
Map<String, String> allValues = await storage.readAll();
|
api: api,
|
||||||
|
product: ProductClient(apiClient: api),
|
||||||
if (allValues.containsKey('token')) {
|
preset: PresetClient(apiClient: api),
|
||||||
token = allValues['token'];
|
meal: MealClient(apiClient: api),
|
||||||
}
|
diary: DiaryClient(apiClient: api),
|
||||||
if (allValues.containsKey('refreshToken')) {
|
entry: EntryClient(apiClient: api));
|
||||||
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
lib/client/based.dart
Normal file
7
lib/client/based.dart
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
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,10 +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/theme.dart';
|
import 'package:fooder/theme.dart';
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
final Context ctx;
|
||||||
|
|
||||||
|
const MyApp({required this.ctx, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -15,14 +17,16 @@ class MyApp extends StatelessWidget {
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.system,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
home: LoginScreen(
|
home: LoginScreen(
|
||||||
apiClient: ApiClient(
|
ctx: ctx,
|
||||||
baseUrl: 'https://fooderapi.domandoman.xyz/api',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() async {
|
||||||
runApp(const MyApp());
|
var ctx = await Context.create(
|
||||||
|
baseUrl: 'https://fooderapi.domandoman.xyz/api',
|
||||||
|
);
|
||||||
|
|
||||||
|
runApp(MyApp(ctx: ctx));
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,4 +32,17 @@ class Diary {
|
||||||
carb = map['carb'] as double,
|
carb = map['carb'] as double,
|
||||||
fat = map['fat'] as double,
|
fat = map['fat'] as double,
|
||||||
fiber = map['fiber'] as double;
|
fiber = map['fiber'] as double;
|
||||||
|
|
||||||
|
Map<String, Object?> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'date': date.toIso8601String(),
|
||||||
|
'meals': meals.map((e) => e.toMap()).toList(),
|
||||||
|
'calories': calories,
|
||||||
|
'protein': protein,
|
||||||
|
'carb': carb,
|
||||||
|
'fat': fat,
|
||||||
|
'fiber': fiber,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,4 +33,18 @@ class Entry {
|
||||||
fat = map['fat'] as double,
|
fat = map['fat'] as double,
|
||||||
fiber = map['fiber'] as double,
|
fiber = map['fiber'] as double,
|
||||||
carb = map['carb'] as double;
|
carb = map['carb'] as double;
|
||||||
|
|
||||||
|
Map<String, Object?> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'grams': grams,
|
||||||
|
'product': product.toMap(),
|
||||||
|
'mealId': mealId,
|
||||||
|
'calories': calories,
|
||||||
|
'protein': protein,
|
||||||
|
'fat': fat,
|
||||||
|
'fiber': fiber,
|
||||||
|
'carb': carb,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,18 @@ class Meal {
|
||||||
fat = map['fat'] as double,
|
fat = map['fat'] as double,
|
||||||
fiber = map['fiber'] as double,
|
fiber = map['fiber'] as double,
|
||||||
diaryId = map['diary_id'] as int;
|
diaryId = map['diary_id'] as int;
|
||||||
|
|
||||||
|
Map<String, Object?> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'order': order,
|
||||||
|
'calories': calories,
|
||||||
|
'protein': protein,
|
||||||
|
'fat': fat,
|
||||||
|
'fiber': fiber,
|
||||||
|
'carb': carb,
|
||||||
|
'diaryId': diaryId,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,4 +25,16 @@ class Preset {
|
||||||
carb = map['carb'] as double,
|
carb = map['carb'] as double,
|
||||||
fat = map['fat'] as double,
|
fat = map['fat'] as double,
|
||||||
fiber = map['fiber'] as double;
|
fiber = map['fiber'] as double;
|
||||||
|
|
||||||
|
Map<String, Object?> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'calories': calories,
|
||||||
|
'protein': protein,
|
||||||
|
'fat': fat,
|
||||||
|
'fiber': fiber,
|
||||||
|
'carb': carb,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,16 +6,19 @@ class Product {
|
||||||
final double carb;
|
final double carb;
|
||||||
final double fat;
|
final double fat;
|
||||||
final double fiber;
|
final double fiber;
|
||||||
|
final int usageCountCached;
|
||||||
|
final String? barcode;
|
||||||
|
|
||||||
Product({
|
Product(
|
||||||
required this.id,
|
{required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.calories,
|
required this.calories,
|
||||||
required this.protein,
|
required this.protein,
|
||||||
required this.carb,
|
required this.carb,
|
||||||
required this.fat,
|
required this.fat,
|
||||||
required this.fiber,
|
required this.fiber,
|
||||||
});
|
this.usageCountCached = 0,
|
||||||
|
this.barcode});
|
||||||
|
|
||||||
Product.fromJson(Map<String, dynamic> map)
|
Product.fromJson(Map<String, dynamic> map)
|
||||||
: id = map['id'] as int,
|
: id = map['id'] as int,
|
||||||
|
@ -24,5 +27,21 @@ class Product {
|
||||||
protein = map['protein'] as double,
|
protein = map['protein'] as double,
|
||||||
carb = map['carb'] as double,
|
carb = map['carb'] as double,
|
||||||
fat = map['fat'] as double,
|
fat = map['fat'] as double,
|
||||||
fiber = map['fiber'] as double;
|
fiber = map['fiber'] as double,
|
||||||
|
usageCountCached = map['usage_count_cached'] as int,
|
||||||
|
barcode = map['barcode'] as String?;
|
||||||
|
|
||||||
|
Map<String, Object?> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'calories': calories,
|
||||||
|
'protein': protein,
|
||||||
|
'carb': carb,
|
||||||
|
'fat': fat,
|
||||||
|
'fiber': fiber,
|
||||||
|
'barcode': barcode,
|
||||||
|
'usage_count_cached': usageCountCached,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +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 this.diary});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AddEntryScreen> createState() => _AddEntryScreen();
|
State<AddEntryScreen> createState() => _AddEntryScreen();
|
||||||
|
@ -26,6 +25,7 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
|
||||||
final productNameController = TextEditingController();
|
final productNameController = TextEditingController();
|
||||||
Meal? meal;
|
Meal? meal;
|
||||||
List<Product> products = [];
|
List<Product> products = [];
|
||||||
|
Diary get diary => widget.diary;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
@ -44,18 +44,16 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
setState(() {
|
setState(() {
|
||||||
meal = widget.diary.meals[0];
|
meal = diary.meals[0];
|
||||||
});
|
});
|
||||||
_getProducts().then((value) => null);
|
_getProducts().then((value) => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getProducts() async {
|
Future<void> _getProducts() async {
|
||||||
var productsMap =
|
var products = await client.product.list(productNameController.text);
|
||||||
await widget.apiClient.getProducts(productNameController.text);
|
|
||||||
setState(() {
|
setState(() {
|
||||||
products = (productsMap['products'] as List<dynamic>)
|
this.products = products;
|
||||||
.map((e) => Product.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +91,7 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await widget.apiClient.addEntry(
|
await client.entry.create(
|
||||||
grams: grams,
|
grams: grams,
|
||||||
productId: products[0].id,
|
productId: products[0].id,
|
||||||
mealId: meal!.id,
|
mealId: meal!.id,
|
||||||
|
@ -111,9 +109,7 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
|
||||||
|
|
||||||
if (res is String) {
|
if (res is String) {
|
||||||
try {
|
try {
|
||||||
var productMap = await widget.apiClient.getProductByBarcode(res);
|
var product = await client.product.getByBarcode(res);
|
||||||
|
|
||||||
var product = Product.fromJson(productMap);
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
products = [product];
|
products = [product];
|
||||||
|
@ -148,7 +144,7 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
items: <DropdownMenuItem<Meal>>[
|
items: <DropdownMenuItem<Meal>>[
|
||||||
for (var meal in widget.diary.meals)
|
for (var meal in diary.meals)
|
||||||
DropdownMenuItem<Meal>(
|
DropdownMenuItem<Meal>(
|
||||||
value: meal,
|
value: meal,
|
||||||
child: Text(meal.name),
|
child: Text(meal.name),
|
||||||
|
@ -179,7 +175,7 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => AddProductScreen(
|
builder: (context) => AddProductScreen(
|
||||||
apiClient: widget.apiClient,
|
ctx: ctx,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((product) {
|
).then((product) {
|
||||||
|
|
|
@ -9,8 +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 this.diary});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AddMealScreen> createState() => _AddMealScreen();
|
State<AddMealScreen> createState() => _AddMealScreen();
|
||||||
|
@ -22,15 +21,13 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
|
||||||
bool nameChanged = false;
|
bool nameChanged = false;
|
||||||
List<Preset> presets = [];
|
List<Preset> presets = [];
|
||||||
Preset? selectedPreset;
|
Preset? selectedPreset;
|
||||||
|
Diary get diary => widget.diary;
|
||||||
|
|
||||||
Future<void> _getPresets() async {
|
Future<void> _getPresets() async {
|
||||||
var presetsMap =
|
var presets = await client.preset.list(presetNameController.text);
|
||||||
await widget.apiClient.getPresets(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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -39,7 +36,7 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
setState(() {
|
setState(() {
|
||||||
nameController.text = "Meal ${widget.diary.meals.length + 1}";
|
nameController.text = "Meal ${diary.meals.length + 1}";
|
||||||
});
|
});
|
||||||
_getPresets();
|
_getPresets();
|
||||||
}
|
}
|
||||||
|
@ -56,22 +53,22 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addMeal() async {
|
Future<void> addMeal() async {
|
||||||
await widget.apiClient.addMeal(
|
await client.meal.create(
|
||||||
name: nameController.text,
|
name: nameController.text,
|
||||||
diaryId: widget.diary.id,
|
diaryId: diary.id,
|
||||||
);
|
);
|
||||||
popMeDaddy();
|
popMeDaddy();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deletePreset(Preset preset) async {
|
Future<void> deletePreset(Preset preset) async {
|
||||||
widget.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) {
|
||||||
|
@ -87,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);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -97,15 +94,15 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addMealFromPreset() async {
|
Future<void> addMealFromPreset() async {
|
||||||
if (selectedPreset == null) {
|
if (selectedPreset == null) {
|
||||||
_addMeal();
|
addMeal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await widget.apiClient.addMealFromPreset(
|
await client.meal.createFromPreset(
|
||||||
name: nameChanged ? nameController.text : selectedPreset!.name,
|
name: nameChanged ? nameController.text : selectedPreset!.name,
|
||||||
diaryId: widget.diary.id,
|
diaryId: diary.id,
|
||||||
presetId: selectedPreset!.id,
|
presetId: selectedPreset!.id,
|
||||||
);
|
);
|
||||||
popMeDaddy();
|
popMeDaddy();
|
||||||
|
@ -140,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,
|
||||||
|
@ -153,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,7 +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({super.key, required super.apiClient});
|
const AddProductScreen({super.key, required super.ctx});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AddProductScreen> createState() => _AddProductScreen();
|
State<AddProductScreen> createState() => _AddProductScreen();
|
||||||
|
@ -37,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 widget.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(
|
||||||
|
@ -194,7 +181,7 @@ class _AddProductScreen extends BasedState<AddProductScreen> {
|
||||||
])),
|
])),
|
||||||
),
|
),
|
||||||
floatingActionButton: FActionButton(
|
floatingActionButton: FActionButton(
|
||||||
onPressed: _addProduct,
|
onPressed: addProduct,
|
||||||
icon: Icons.save,
|
icon: Icons.save,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
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/components/app_bar.dart';
|
import 'package:fooder/components/app_bar.dart';
|
||||||
import 'package:fooder/components/navigation_bar.dart';
|
import 'package:fooder/components/navigation_bar.dart';
|
||||||
import 'package:fooder/screens/login.dart';
|
import 'package:fooder/screens/login.dart';
|
||||||
import 'package:fooder/screens/main.dart';
|
import 'package:fooder/screens/main.dart';
|
||||||
|
import 'package:fooder/screens/settings.dart';
|
||||||
|
|
||||||
TextStyle logoStyle(context) {
|
TextStyle logoStyle(context) {
|
||||||
return Theme.of(context).textTheme.labelLarge!.copyWith(
|
return Theme.of(context).textTheme.labelLarge!.copyWith(
|
||||||
|
@ -12,27 +15,46 @@ TextStyle logoStyle(context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class BasedScreen extends StatefulWidget {
|
abstract class BasedScreen extends StatefulWidget {
|
||||||
final ApiClient apiClient;
|
final Context ctx;
|
||||||
|
|
||||||
const BasedScreen({super.key, required this.apiClient});
|
const BasedScreen({super.key, required this.ctx});
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class BasedState<T extends BasedScreen> extends State<T> {
|
abstract class BasedState<T extends BasedScreen> extends State<T> {
|
||||||
void _logout() async {
|
Context get ctx => widget.ctx;
|
||||||
await widget.apiClient.logout();
|
Client get client => widget.ctx.client;
|
||||||
|
Storage get storage => widget.ctx.storage;
|
||||||
|
|
||||||
|
void logout() async {
|
||||||
|
await client.api.logout();
|
||||||
|
backToLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void backToLogin() {
|
||||||
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => LoginScreen(apiClient: widget.apiClient),
|
builder: (context) => LoginScreen(ctx: ctx),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void backToDiary() {
|
void backToDiary() {
|
||||||
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => MainScreen(apiClient: widget.apiClient),
|
builder: (context) => MainScreen(ctx: ctx),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void navigateToSettings() {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => SettingsScreen(ctx: ctx),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -45,7 +67,7 @@ abstract class BasedState<T extends BasedScreen> extends State<T> {
|
||||||
Icons.logout,
|
Icons.logout,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
onPressed: _logout,
|
onPressed: logout,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -83,7 +105,7 @@ abstract class BasedState<T extends BasedScreen> extends State<T> {
|
||||||
Icons.person,
|
Icons.person,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
onPressed: () {},
|
onPressed: navigateToSettings,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -91,6 +113,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,8 +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 this.entry});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<EditEntryScreen> createState() => _EditEntryScreen();
|
State<EditEntryScreen> createState() => _EditEntryScreen();
|
||||||
|
@ -23,6 +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 => widget.entry;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
@ -39,57 +39,45 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
setState(() {
|
setState(() {
|
||||||
gramsController.text = widget.entry.grams.toString();
|
gramsController.text = entry.grams.toString();
|
||||||
productNameController.text = widget.entry.product.name;
|
productNameController.text = entry.product.name;
|
||||||
products = [widget.entry.product];
|
products = [entry.product];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getProducts() async {
|
Future<void> getProducts() async {
|
||||||
var productsMap =
|
var products = await client.product.list(productNameController.text);
|
||||||
await widget.apiClient.getProducts(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 widget.apiClient.updateEntry(
|
await client.entry.update(
|
||||||
widget.entry.id,
|
entry.id,
|
||||||
grams: grams,
|
grams: grams,
|
||||||
productId: products[0].id,
|
productId: products[0].id,
|
||||||
mealId: widget.entry.mealId,
|
mealId: entry.mealId,
|
||||||
);
|
);
|
||||||
popMeDaddy();
|
popMeDaddy();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteEntry() async {
|
Future<void> deleteEntry() async {
|
||||||
await widget.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(
|
||||||
|
@ -99,9 +87,7 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
|
||||||
|
|
||||||
if (res is String) {
|
if (res is String) {
|
||||||
try {
|
try {
|
||||||
var productMap = await widget.apiClient.getProductByBarcode(res);
|
var product = await client.product.getByBarcode(res);
|
||||||
|
|
||||||
var product = Product.fromJson(productMap);
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
products = [product];
|
products = [product];
|
||||||
|
@ -125,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(
|
||||||
|
@ -144,7 +130,7 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => AddProductScreen(
|
builder: (context) => AddProductScreen(
|
||||||
apiClient: widget.apiClient,
|
ctx: ctx,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((product) {
|
).then((product) {
|
||||||
|
@ -177,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,7 +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({super.key, required super.apiClient});
|
const LoginScreen({super.key, required super.ctx});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LoginScreen> createState() => _LoginScreen();
|
State<LoginScreen> createState() => _LoginScreen();
|
||||||
|
@ -28,15 +28,15 @@ class _LoginScreen extends BasedState<LoginScreen> {
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => MainScreen(apiClient: widget.apiClient),
|
builder: (context) => MainScreen(ctx: ctx),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// login client when button pressed
|
// login client when button pressed
|
||||||
Future<void> _login() async {
|
Future<void> login() async {
|
||||||
try {
|
try {
|
||||||
await widget.apiClient.login(
|
await client.api.login(
|
||||||
usernameController.text,
|
usernameController.text,
|
||||||
passwordController.text,
|
passwordController.text,
|
||||||
);
|
);
|
||||||
|
@ -58,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 widget.apiClient.loadToken();
|
await client.api.loadToken();
|
||||||
|
|
||||||
if (widget.apiClient.refreshToken == null) {
|
if (client.api.refreshToken == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await widget.apiClient.refresh();
|
await client.api.refresh();
|
||||||
showText("Welcome back!");
|
showText("Welcome back!");
|
||||||
popMeDaddy();
|
popMeDaddy();
|
||||||
} on Exception catch (_) {
|
} on Exception catch (_) {
|
||||||
|
@ -105,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),
|
||||||
|
@ -120,8 +120,9 @@ class _LoginScreen extends BasedState<LoginScreen> {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) => RegisterScreen(
|
||||||
RegisterScreen(apiClient: widget.apiClient),
|
ctx: ctx,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,7 +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({super.key, required super.apiClient});
|
const MainScreen({super.key, required super.ctx});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MainScreen> createState() => _MainScreen();
|
State<MainScreen> createState() => _MainScreen();
|
||||||
|
@ -27,10 +27,10 @@ class _MainScreen extends BasedState<MainScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _asyncInitState() async {
|
Future<void> _asyncInitState() async {
|
||||||
var diaryMap = await widget.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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -51,8 +51,7 @@ class _MainScreen extends BasedState<MainScreen> {
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) => AddEntryScreen(ctx: ctx, diary: diary!),
|
||||||
AddEntryScreen(apiClient: widget.apiClient, diary: diary!),
|
|
||||||
),
|
),
|
||||||
).then((_) => _asyncInitState());
|
).then((_) => _asyncInitState());
|
||||||
}
|
}
|
||||||
|
@ -66,7 +65,7 @@ class _MainScreen extends BasedState<MainScreen> {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => AddMealScreen(
|
builder: (context) => AddMealScreen(
|
||||||
apiClient: widget.apiClient,
|
ctx: ctx,
|
||||||
diary: diary!,
|
diary: diary!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -89,13 +88,12 @@ class _MainScreen extends BasedState<MainScreen> {
|
||||||
[
|
[
|
||||||
SummaryWidget(
|
SummaryWidget(
|
||||||
diary: diary!,
|
diary: diary!,
|
||||||
apiClient: widget.apiClient,
|
|
||||||
refreshParent: _asyncInitState,
|
refreshParent: _asyncInitState,
|
||||||
),
|
),
|
||||||
for (var (i, meal) in diary!.meals.indexed)
|
for (var (i, meal) in diary!.meals.indexed)
|
||||||
MealWidget(
|
MealWidget(
|
||||||
|
ctx: ctx,
|
||||||
meal: meal,
|
meal: meal,
|
||||||
apiClient: widget.apiClient,
|
|
||||||
refreshParent: _asyncInitState,
|
refreshParent: _asyncInitState,
|
||||||
initiallyExpanded: i == 0,
|
initiallyExpanded: i == 0,
|
||||||
showText: showText,
|
showText: showText,
|
||||||
|
|
|
@ -5,7 +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({super.key, required super.apiClient});
|
const RegisterScreen({super.key, required super.ctx});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RegisterScreen> createState() => _RegisterScreen();
|
State<RegisterScreen> createState() => _RegisterScreen();
|
||||||
|
@ -49,7 +49,7 @@ class _RegisterScreen extends BasedState<RegisterScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await widget.apiClient.register(
|
await client.api.register(
|
||||||
usernameController.text,
|
usernameController.text,
|
||||||
passwordController.text,
|
passwordController.text,
|
||||||
);
|
);
|
||||||
|
|
56
lib/screens/settings.dart
Normal file
56
lib/screens/settings.dart
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fooder/screens/based.dart';
|
||||||
|
import 'package:fooder/components/button.dart';
|
||||||
|
|
||||||
|
class SettingsScreen extends BasedScreen {
|
||||||
|
const SettingsScreen({super.key, required super.ctx});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsScreen> createState() => _SettingsScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsScreen extends BasedState<SettingsScreen> {
|
||||||
|
Future<void> resetStorage() async {
|
||||||
|
try {
|
||||||
|
ctx.storage.reset();
|
||||||
|
showText("Storage reset");
|
||||||
|
} catch (e) {
|
||||||
|
showError(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
var colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
extendBodyBehindAppBar: false,
|
||||||
|
extendBody: true,
|
||||||
|
appBar: appBar(),
|
||||||
|
bottomNavigationBar: navBar(),
|
||||||
|
body: Center(
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 600),
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: AutofillGroup(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.verified_user_sharp,
|
||||||
|
size: 100,
|
||||||
|
color: colorScheme.primary.withOpacity(0.85),
|
||||||
|
),
|
||||||
|
FButton(
|
||||||
|
labelText: 'Reset local storage',
|
||||||
|
onPressed: resetStorage,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
47
lib/storage.dart
Normal file
47
lib/storage.dart
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'package:fooder/storage/product.dart';
|
||||||
|
import 'package:fooder/storage/diary.dart';
|
||||||
|
|
||||||
|
class Storage {
|
||||||
|
Database db;
|
||||||
|
ProductStorage product;
|
||||||
|
DiaryStorage diary;
|
||||||
|
|
||||||
|
static const String path = "storage.db";
|
||||||
|
|
||||||
|
Storage({required this.db, required this.product, required this.diary});
|
||||||
|
|
||||||
|
static Future<Storage> create() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
var db = await openDatabase(
|
||||||
|
join(await getDatabasesPath(), path),
|
||||||
|
onCreate: createTables,
|
||||||
|
version: 1,
|
||||||
|
);
|
||||||
|
return Storage(
|
||||||
|
db: db, product: ProductStorage(db: db), diary: DiaryStorage(db: db));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reset() async {
|
||||||
|
await db.close();
|
||||||
|
await deleteDatabase(join(await getDatabasesPath(), path));
|
||||||
|
db = await openDatabase(
|
||||||
|
join(await getDatabasesPath(), path),
|
||||||
|
onCreate: createTables,
|
||||||
|
version: 1,
|
||||||
|
);
|
||||||
|
product = ProductStorage(db: db);
|
||||||
|
diary = DiaryStorage(db: db);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> createTables(Database db, int version) async {
|
||||||
|
var batch = db.batch();
|
||||||
|
await ProductStorage.createTable(batch);
|
||||||
|
await DiaryStorage.createTable(batch);
|
||||||
|
await batch.commit(noResult: true);
|
||||||
|
}
|
||||||
|
}
|
11
lib/storage/based.dart
Normal file
11
lib/storage/based.dart
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
|
abstract class StorageBased {
|
||||||
|
Database db;
|
||||||
|
|
||||||
|
StorageBased({required this.db});
|
||||||
|
|
||||||
|
static Future<void> createTable(Batch batch) async {}
|
||||||
|
}
|
43
lib/storage/diary.dart
Normal file
43
lib/storage/diary.dart
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'package:fooder/models/diary.dart';
|
||||||
|
import 'package:fooder/storage/based.dart';
|
||||||
|
|
||||||
|
class DiaryStorage extends StorageBased {
|
||||||
|
DiaryStorage({required super.db});
|
||||||
|
|
||||||
|
static Future<void> createTable(Batch batch) async {
|
||||||
|
batch.execute('''
|
||||||
|
CREATE TABLE diary(
|
||||||
|
date TEXT PRIMARY KEY,
|
||||||
|
content TEXT,
|
||||||
|
needs_sync BOOLEAN,
|
||||||
|
last_sync TEXT
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Diary?> get({required DateTime date}) async {
|
||||||
|
var result = await db
|
||||||
|
.query('diary', where: 'date = ?', whereArgs: [date.toIso8601String()]);
|
||||||
|
if (result.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Diary.fromJson(
|
||||||
|
jsonDecode((result.first as Map<String, dynamic>)['content']));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> insert(Diary diary, {bool needsSync = false}) async {
|
||||||
|
var data = {
|
||||||
|
'id': diary.id,
|
||||||
|
'date': diary.date.toIso8601String(),
|
||||||
|
'content': jsonEncode(diary.toMap()),
|
||||||
|
'needs_sync': needsSync,
|
||||||
|
'last_sync': needsSync ? DateTime.now().toIso8601String() : null,
|
||||||
|
};
|
||||||
|
await db.insert('diary', data,
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||||
|
}
|
||||||
|
}
|
0
lib/storage/preset.dart
Normal file
0
lib/storage/preset.dart
Normal file
55
lib/storage/product.dart
Normal file
55
lib/storage/product.dart
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'package:fooder/models/product.dart';
|
||||||
|
import 'package:fooder/storage/based.dart';
|
||||||
|
|
||||||
|
class ProductStorage extends StorageBased {
|
||||||
|
ProductStorage({required super.db});
|
||||||
|
|
||||||
|
static Future<void> createTable(Batch batch) async {
|
||||||
|
batch.execute('''
|
||||||
|
CREATE TABLE product(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
barcode TEXT,
|
||||||
|
calories REAL,
|
||||||
|
protein REAL,
|
||||||
|
carb REAL,
|
||||||
|
fat REAL,
|
||||||
|
fiber REAL,
|
||||||
|
usage_count_cached INTEGER
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Product>> list({String? name, String? barcode}) async {
|
||||||
|
var result = await db.query('product',
|
||||||
|
where: 'name LIKE ? AND barcode LIKE ?',
|
||||||
|
whereArgs: ['%$name%', '%$barcode%'],
|
||||||
|
orderBy: 'usage_count_cached DESC');
|
||||||
|
return result.map((e) => Product.fromJson(e)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Product?> get(int id) async {
|
||||||
|
var result = await db.query('product', where: 'id = ?', whereArgs: [id]);
|
||||||
|
if (result.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Product.fromJson(result.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> insert(Product product) async {
|
||||||
|
await db.insert('product', product.toMap(),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> bulkInsert(List<Product> products) async {
|
||||||
|
var batch = db.batch();
|
||||||
|
for (var product in products) {
|
||||||
|
batch.insert('product', product.toMap(),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||||
|
}
|
||||||
|
await batch.commit(noResult: true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +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 'dart:core';
|
import 'dart:core';
|
||||||
|
|
||||||
class MealHeader extends StatelessWidget {
|
class MealHeader extends StatelessWidget {
|
||||||
|
@ -37,7 +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 Function() refreshParent;
|
final Function() refreshParent;
|
||||||
final Function(String) showText;
|
final Function(String) showText;
|
||||||
final bool initiallyExpanded;
|
final bool initiallyExpanded;
|
||||||
|
@ -45,7 +45,7 @@ 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,
|
||||||
|
@ -74,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");
|
||||||
},
|
},
|
||||||
|
@ -85,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,
|
||||||
|
@ -106,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");
|
||||||
},
|
},
|
||||||
|
@ -122,7 +122,7 @@ class MealWidget extends StatelessWidget {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => EditEntryScreen(
|
builder: (context) => EditEntryScreen(
|
||||||
apiClient: apiClient,
|
ctx: ctx,
|
||||||
entry: entry,
|
entry: entry,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -191,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) {
|
||||||
|
|
|
@ -7,8 +7,10 @@ import Foundation
|
||||||
|
|
||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
|
import sqflite
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,15 @@ PODS:
|
||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- sqflite (0.0.3):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
|
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
flutter_secure_storage_macos:
|
flutter_secure_storage_macos:
|
||||||
|
@ -18,11 +22,14 @@ EXTERNAL SOURCES:
|
||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||||
|
sqflite:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea
|
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
|
||||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
|
|
||||||
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
|
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
|
||||||
|
|
||||||
|
|
|
@ -4,29 +4,52 @@
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Fooder</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string></string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>fooder_web</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
<true/>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
<string>LaunchScreen</string>
|
||||||
<key>NSMainNibFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>MainMenu</string>
|
<string>Main</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<string>NSApplication</string>
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<false/>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>App needs access to photo lib for profile images</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>To capture profile photo please grant camera access</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
74
pubspec.lock
74
pubspec.lock
|
@ -5,10 +5,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265
|
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.5.1"
|
version: "3.6.1"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -178,10 +178,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage
|
name: flutter_secure_storage
|
||||||
sha256: c0f402067fb0498934faa6bddd670de0a3db45222e2ca9a068c6177c9a2360a4
|
sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.1.1"
|
version: "9.2.2"
|
||||||
flutter_secure_storage_linux:
|
flutter_secure_storage_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -194,18 +194,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_macos
|
name: flutter_secure_storage_macos
|
||||||
sha256: "8cfa53010a294ff095d7be8fa5bb15f2252c50018d69c5104851303f3ff92510"
|
sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.2"
|
||||||
flutter_secure_storage_platform_interface:
|
flutter_secure_storage_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_platform_interface
|
name: flutter_secure_storage_platform_interface
|
||||||
sha256: "301f67ee9b87f04aef227f57f13f126fa7b13543c8e7a93f25c5d2d534c28a4a"
|
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.2"
|
||||||
flutter_secure_storage_web:
|
flutter_secure_storage_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -244,10 +244,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
|
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.2"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -260,10 +260,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image
|
name: image
|
||||||
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
|
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.7"
|
version: "4.2.0"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -353,7 +353,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.0"
|
version: "1.11.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||||
|
@ -364,10 +364,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
|
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.4"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -404,10 +404,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_windows
|
name: path_provider_windows
|
||||||
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
|
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.3.0"
|
||||||
permission_handler:
|
permission_handler:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -420,26 +420,26 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_android
|
name: permission_handler_android
|
||||||
sha256: "8bb852cd759488893805c3161d0b2b5db55db52f773dbb014420b304055ba2c5"
|
sha256: b29a799ca03be9f999aa6c39f7de5209482d638e6f857f6b93b0875c618b7e54
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.0.6"
|
version: "12.0.7"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_apple
|
name: permission_handler_apple
|
||||||
sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662
|
sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.4.4"
|
version: "9.4.5"
|
||||||
permission_handler_html:
|
permission_handler_html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_html
|
name: permission_handler_html
|
||||||
sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d"
|
sha256: "6cac773d389e045a8d4f85418d07ad58ef9e42a56e063629ce14c4c26344de24"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.1"
|
version: "0.1.2"
|
||||||
permission_handler_platform_interface:
|
permission_handler_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -468,10 +468,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.5"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -501,6 +501,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.10.0"
|
||||||
|
sqflite:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.3+1"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.4"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -525,6 +541,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0+1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -44,6 +44,8 @@ dependencies:
|
||||||
blur: ^3.1.0
|
blur: ^3.1.0
|
||||||
marquee: ^2.2.3
|
marquee: ^2.2.3
|
||||||
flutter_launcher_icons: ^0.13.1
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
sqflite: ^2.3.3+1
|
||||||
|
path: ^1.9.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_launcher_icons: ^0.13.1
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
|
Loading…
Reference in a new issue