diff --git a/lib/client.dart b/lib/client.dart index 83b93df..1ba74f4 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -1,328 +1,35 @@ -import 'package:http/http.dart' as http; -import 'dart:convert'; -import 'package:intl/intl.dart'; -import 'package:fooder/models/meal.dart'; +import 'package:fooder/client/api.dart'; +import 'package:fooder/client/product.dart'; +import 'package:fooder/client/entry.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 { - final String baseUrl; - String? token; - String? refreshToken; - http.Client httpClient = http.Client(); - final FlutterSecureStorage storage = const FlutterSecureStorage(); + Client( + {required this.api, + required this.product, + required this.entry, + required this.meal, + required this.diary, + required this.preset}); - ApiClient({ - required this.baseUrl, - }); + static Future create({required String baseUrl}) async { + var api = await ApiClient.create(baseUrl: baseUrl); - static Future create({required String baseUrl}) async { - var client = ApiClient(baseUrl: baseUrl); - client.loadToken(); - return client; - } - - Future loadToken() async { - Map allValues = await storage.readAll(); - - if (allValues.containsKey('token')) { - token = allValues['token']; - } - if (allValues.containsKey('refreshToken')) { - refreshToken = allValues['refreshToken']; - } - } - - Map 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 _jsonDecode(http.Response response) { - try { - return jsonDecode(utf8.decode(response.bodyBytes)); - } catch (e) { - throw Exception('Response returned status code: ${response.statusCode}'); - } - } - - Future> 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> post(String path, Map 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 postNoResult(String path, Map 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 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> patch( - String path, Map 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 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 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> 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 logout() async { - token = null; - refreshToken = null; - - await storage.deleteAll(); - } - - Future> getProducts(String q) async { - var response = - await get("/product?${Uri(queryParameters: {"q": q}).query}"); - return response; - } - - Future> getProductByBarcode(String barcode) async { - var response = await get("/product/by_barcode?${Uri(queryParameters: { - "barcode": barcode - }).query}"); - return response; - } - - Future> getPresets(String? q) async { - var response = await get("/preset?${Uri(queryParameters: {"q": q}).query}"); - return response; - } - - Future 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 deleteEntry(int id) async { - await delete("/entry/$id"); - } - - Future deleteMeal(int id) async { - await delete("/meal/$id"); - } - - Future deletePreset(int id) async { - await delete("/preset/$id"); - } - - Future 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 register(String username, String password) async { - try { - await post( - "/user", - { - "username": username, - "password": password, - }, - forLogin: true, - ); - } catch (e) { - throw Exception("Failed to register"); - } - } - - Future addMeal({required String name, required int diaryId}) async { - await post("/meal", { - "name": name, - "diary_id": diaryId, - }); - } - - Future 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> 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 saveMeal(Meal meal, String name) async { - await postNoResult("/meal/${meal.id}/save", { - "name": name, - }); + return Client( + api: api, + product: ProductClient(apiClient: api), + preset: PresetClient(apiClient: api), + meal: MealClient(apiClient: api), + diary: DiaryClient(apiClient: api), + entry: EntryClient(apiClient: api)); } } diff --git a/lib/client/api.dart b/lib/client/api.dart new file mode 100644 index 0000000..57c8114 --- /dev/null +++ b/lib/client/api.dart @@ -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 create({required String baseUrl}) async { + var client = ApiClient(baseUrl: baseUrl); + client.loadToken(); + return client; + } + + Future loadToken() async { + Map allValues = await storage.readAll(); + + if (allValues.containsKey('token')) { + token = allValues['token']; + } + if (allValues.containsKey('refreshToken')) { + refreshToken = allValues['refreshToken']; + } + } + + Map 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 _jsonDecode(http.Response response) { + try { + return jsonDecode(utf8.decode(response.bodyBytes)); + } catch (e) { + throw Exception('Response returned status code: ${response.statusCode}'); + } + } + + Future> 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> post(String path, Map 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 postNoResult(String path, Map 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 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> patch( + String path, Map 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 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 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 logout() async { + token = null; + refreshToken = null; + + await storage.deleteAll(); + } + + Future register(String username, String password) async { + try { + await post( + "/user", + { + "username": username, + "password": password, + }, + forLogin: true, + ); + } catch (e) { + throw Exception("Failed to register"); + } + } +} diff --git a/lib/client/based.dart b/lib/client/based.dart index e0752d9..dd7fab2 100644 --- a/lib/client/based.dart +++ b/lib/client/based.dart @@ -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}); +} diff --git a/lib/client/diary.dart b/lib/client/diary.dart new file mode 100644 index 0000000..bd5b621 --- /dev/null +++ b/lib/client/diary.dart @@ -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 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); + } +} diff --git a/lib/client/entry.dart b/lib/client/entry.dart new file mode 100644 index 0000000..2c92efb --- /dev/null +++ b/lib/client/entry.dart @@ -0,0 +1,36 @@ +import 'package:fooder/client/based.dart'; + +class EntryClient extends BasedClient { + const EntryClient({required super.apiClient}); + + Future 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 delete(int id) async { + await apiClient.delete("/entry/$id"); + } + + Future 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); + } +} diff --git a/lib/client/meal.dart b/lib/client/meal.dart new file mode 100644 index 0000000..85857c8 --- /dev/null +++ b/lib/client/meal.dart @@ -0,0 +1,33 @@ +import 'package:fooder/client/based.dart'; + +class MealClient extends BasedClient { + const MealClient({required super.apiClient}); + + Future create({required String name, required int diaryId}) async { + await apiClient.post("/meal", { + "name": name, + "diary_id": diaryId, + }); + } + + Future 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 update(int id, String name) async { + await apiClient.postNoResult("/meal/$id/save", { + "name": name, + }); + } + + Future delete(int id) async { + await apiClient.delete("/meal/$id"); + } +} diff --git a/lib/client/preset.dart b/lib/client/preset.dart new file mode 100644 index 0000000..dc3b1fb --- /dev/null +++ b/lib/client/preset.dart @@ -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(String? q) async { + var response = + await apiClient.get("/preset?${Uri(queryParameters: {"q": q}).query}"); + return (response['presets'] as List) + .map((e) => Preset.fromJson(e as Map)) + .toList(); + } + + Future delete(int id) async { + await apiClient.delete("/preset/$id"); + } +} diff --git a/lib/client/product.dart b/lib/client/product.dart new file mode 100644 index 0000000..f7a821b --- /dev/null +++ b/lib/client/product.dart @@ -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(String q) async { + var response = + await apiClient.get("/product?${Uri(queryParameters: {"q": q}).query}"); + + return (response['products'] as List) + .map((e) => Product.fromJson(e as Map)) + .toList(); + } + + Future getByBarcode(String barcode) async { + var response = await apiClient.get( + "/product/by_barcode?${Uri(queryParameters: { + "barcode": barcode + }).query}"); + + return Product.fromJson(response); + } + + Future 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); + } +} diff --git a/lib/context.dart b/lib/context.dart new file mode 100644 index 0000000..d8cd9a3 --- /dev/null +++ b/lib/context.dart @@ -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 create({required String baseUrl}) async { + var client = await Client.create(baseUrl: baseUrl); + var storage = await Storage.create(); + + return Context(client: client, storage: storage); + } +} diff --git a/lib/main.dart b/lib/main.dart index cbd0746..36452f0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,14 +1,12 @@ import 'package:flutter/material.dart'; import 'package:fooder/screens/login.dart'; -import 'package:fooder/client.dart'; -import 'package:fooder/storage.dart'; +import 'package:fooder/context.dart'; import 'package:fooder/theme.dart'; class MyApp extends StatelessWidget { - final Storage storage; - final ApiClient apiClient; + final Context ctx; - const MyApp({required this.storage, required this.apiClient, super.key}); + const MyApp({required this.ctx, super.key}); @override Widget build(BuildContext context) { @@ -19,18 +17,16 @@ class MyApp extends StatelessWidget { themeMode: ThemeMode.system, debugShowCheckedModeBanner: false, home: LoginScreen( - apiClient: apiClient, - storage: storage, + ctx: ctx, ), ); } } void main() async { - var storage = await Storage.create(); - var apiClient = await ApiClient.create( + var ctx = await Context.create( baseUrl: 'https://fooderapi.domandoman.xyz/api', ); - runApp(MyApp(storage: storage, apiClient: apiClient)); + runApp(MyApp(ctx: ctx)); } diff --git a/lib/screens/add_entry.dart b/lib/screens/add_entry.dart index abb0db1..170f1a7 100644 --- a/lib/screens/add_entry.dart +++ b/lib/screens/add_entry.dart @@ -14,11 +14,7 @@ import 'package:simple_barcode_scanner/simple_barcode_scanner.dart'; class AddEntryScreen extends BasedScreen { final Diary diary; - const AddEntryScreen( - {super.key, - required super.apiClient, - required super.storage, - required this.diary}); + const AddEntryScreen({super.key, required super.ctx, required this.diary}); @override State createState() => _AddEntryScreen(); @@ -54,14 +50,10 @@ class _AddEntryScreen extends BasedState { } Future _getProducts() async { - var productsMap = await apiClient.getProducts(productNameController.text); - - var parsedProducts = (productsMap['products'] as List) - .map((e) => Product.fromJson(e as Map)) - .toList(); + var products = await client.product.list(productNameController.text); setState(() { - products = parsedProducts; + this.products = products; }); } @@ -99,7 +91,7 @@ class _AddEntryScreen extends BasedState { return; } - await apiClient.addEntry( + await client.entry.create( grams: grams, productId: products[0].id, mealId: meal!.id, @@ -117,9 +109,7 @@ class _AddEntryScreen extends BasedState { if (res is String) { try { - var productMap = await apiClient.getProductByBarcode(res); - - var product = Product.fromJson(productMap); + var product = await client.product.getByBarcode(res); setState(() { products = [product]; @@ -185,8 +175,7 @@ class _AddEntryScreen extends BasedState { context, MaterialPageRoute( builder: (context) => AddProductScreen( - apiClient: apiClient, - storage: storage, + ctx: ctx, ), ), ).then((product) { diff --git a/lib/screens/add_meal.dart b/lib/screens/add_meal.dart index cf14e70..2dbfc6b 100644 --- a/lib/screens/add_meal.dart +++ b/lib/screens/add_meal.dart @@ -9,11 +9,7 @@ import 'package:fooder/components/floating_action_button.dart'; class AddMealScreen extends BasedScreen { final Diary diary; - const AddMealScreen( - {super.key, - required super.apiClient, - required super.storage, - required this.diary}); + const AddMealScreen({super.key, required super.ctx, required this.diary}); @override State createState() => _AddMealScreen(); @@ -28,12 +24,10 @@ class _AddMealScreen extends BasedState { Diary get diary => widget.diary; Future _getPresets() async { - var presetsMap = await apiClient.getPresets(presetNameController.text); + var presets = await client.preset.list(presetNameController.text); setState(() { - presets = (presetsMap['presets'] as List) - .map((e) => Preset.fromJson(e as Map)) - .toList(); + this.presets = presets; selectedPreset = null; }); } @@ -59,22 +53,22 @@ class _AddMealScreen extends BasedState { ); } - Future _addMeal() async { - await apiClient.addMeal( + Future addMeal() async { + await client.meal.create( name: nameController.text, diaryId: diary.id, ); popMeDaddy(); } - Future _deletePreset(Preset preset) async { - apiClient.deletePreset(preset.id); + Future deletePreset(Preset preset) async { + client.preset.delete(preset.id); setState(() { presets.remove(preset); }); } - Future deletePreset(context, Preset preset) async { + Future deletePresetPopup(context, Preset preset) async { showDialog( context: context, builder: (context) { @@ -90,7 +84,7 @@ class _AddMealScreen extends BasedState { IconButton( icon: const Icon(Icons.delete), onPressed: () { - _deletePreset(preset); + deletePreset(preset); Navigator.pop(context); }, ), @@ -100,13 +94,13 @@ class _AddMealScreen extends BasedState { ); } - Future _addMealFromPreset() async { + Future addMealFromPreset() async { if (selectedPreset == null) { - _addMeal(); + addMeal(); return; } - await apiClient.addMealFromPreset( + await client.meal.createFromPreset( name: nameChanged ? nameController.text : selectedPreset!.name, diaryId: diary.id, presetId: selectedPreset!.id, @@ -143,10 +137,10 @@ class _AddMealScreen extends BasedState { presetNameController.text = preset.name; selectedPreset = preset; }); - _addMealFromPreset(); + addMealFromPreset(); }, onLongPress: () { - deletePreset(context, preset); + deletePresetPopup(context, preset); }, title: PresetWidget( preset: preset, @@ -156,7 +150,7 @@ class _AddMealScreen extends BasedState { ), ), floatingActionButton: FActionButton( - onPressed: _addMealFromPreset, + onPressed: addMealFromPreset, icon: Icons.playlist_add_rounded, ), ); diff --git a/lib/screens/add_product.dart b/lib/screens/add_product.dart index 4efa4cc..efe3685 100644 --- a/lib/screens/add_product.dart +++ b/lib/screens/add_product.dart @@ -7,8 +7,7 @@ import 'package:fooder/components/text.dart'; import 'package:fooder/components/floating_action_button.dart'; class AddProductScreen extends BasedScreen { - const AddProductScreen( - {super.key, required super.apiClient, required super.storage}); + const AddProductScreen({super.key, required super.ctx}); @override State createState() => _AddProductScreen(); @@ -38,38 +37,25 @@ class _AddProductScreen extends BasedState { ); } - Future _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; - } - } - - Future _addProduct() async { - var carb = await _parseDouble(carbController.text, "Carbs"); - var fat = await _parseDouble(fatController.text, "Fat"); - var protein = await _parseDouble(proteinController.text, "Protein"); + Future 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 = - await _parseDouble(fiberController.text, "Fiber", silent: true) ?? 0; + await parseDouble(fiberController.text, "Fiber", silent: true) ?? 0; if (carb == null || fat == null || protein == null) { return; } try { - var productJson = await apiClient.addProduct( + var product = await client.product.create( carb: carb, fat: fat, protein: protein, fiber: fiber, name: nameController.text, ); - var product = Product.fromJson(productJson); popMeDaddy(product); } catch (e) { showError( @@ -195,7 +181,7 @@ class _AddProductScreen extends BasedState { ])), ), floatingActionButton: FActionButton( - onPressed: _addProduct, + onPressed: addProduct, icon: Icons.save, ), ); diff --git a/lib/screens/based.dart b/lib/screens/based.dart index b157b2a..828b145 100644 --- a/lib/screens/based.dart +++ b/lib/screens/based.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:fooder/context.dart'; import 'package:fooder/client.dart'; import 'package:fooder/storage.dart'; import 'package:fooder/components/app_bar.dart'; @@ -13,19 +14,18 @@ TextStyle logoStyle(context) { } abstract class BasedScreen extends StatefulWidget { - final ApiClient apiClient; - final Storage storage; + final Context ctx; - const BasedScreen( - {super.key, required this.apiClient, required this.storage}); + const BasedScreen({super.key, required this.ctx}); } abstract class BasedState extends State { - ApiClient get apiClient => widget.apiClient; - Storage get storage => widget.storage; + Context get ctx => widget.ctx; + Client get client => widget.ctx.client; + Storage get storage => widget.ctx.storage; void logout() async { - await apiClient.logout(); + await client.api.logout(); backToLogin(); } @@ -33,8 +33,7 @@ abstract class BasedState extends State { Navigator.pushReplacement( context, MaterialPageRoute( - builder: (context) => - LoginScreen(apiClient: apiClient, storage: storage), + builder: (context) => LoginScreen(ctx: ctx), ), ); } @@ -43,8 +42,7 @@ abstract class BasedState extends State { Navigator.pushReplacement( context, MaterialPageRoute( - builder: (context) => - MainScreen(apiClient: apiClient, storage: storage), + builder: (context) => MainScreen(ctx: ctx), ), ); } @@ -103,6 +101,18 @@ abstract class BasedState extends State { ); } + Future 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) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/screens/edit_entry.dart b/lib/screens/edit_entry.dart index 3db6ece..209c166 100644 --- a/lib/screens/edit_entry.dart +++ b/lib/screens/edit_entry.dart @@ -12,11 +12,7 @@ import 'package:simple_barcode_scanner/simple_barcode_scanner.dart'; class EditEntryScreen extends BasedScreen { final Entry entry; - const EditEntryScreen( - {super.key, - required super.apiClient, - required super.storage, - required this.entry}); + const EditEntryScreen({super.key, required super.ctx, required this.entry}); @override State createState() => _EditEntryScreen(); @@ -26,7 +22,7 @@ class _EditEntryScreen extends BasedState { final gramsController = TextEditingController(); final productNameController = TextEditingController(); List products = []; - Entry get entry => entry; + Entry get entry => widget.entry; @override void dispose() { @@ -49,36 +45,25 @@ class _EditEntryScreen extends BasedState { }); } - Future _getProducts() async { - var productsMap = await apiClient.getProducts(productNameController.text); + Future getProducts() async { + var products = await client.product.list(productNameController.text); setState(() { - products = (productsMap['products'] as List) - .map((e) => Product.fromJson(e as Map)) - .toList(); + this.products = products; }); } - Future _parseDouble(String text, String name) async { - try { - return double.parse(text.replaceAll(",", ".")); - } catch (e) { - showError("$name must be a number"); - return null; - } - } - - Future _saveEntry() async { + Future saveEntry() async { if (products.length != 1) { showError("Pick product first"); return; } - var grams = await _parseDouble(gramsController.text, "Grams"); + var grams = await parseDouble(gramsController.text, "Grams"); if (grams == null) { return; } - await apiClient.updateEntry( + await client.entry.update( entry.id, grams: grams, productId: products[0].id, @@ -87,12 +72,12 @@ class _EditEntryScreen extends BasedState { popMeDaddy(); } - Future _deleteEntry() async { - await apiClient.deleteEntry(widget.entry.id); + Future deleteEntry() async { + await client.entry.delete(widget.entry.id); popMeDaddy(); } - Future _findProductByBarCode() async { + Future findProductByBarCode() async { var res = await Navigator.push( context, MaterialPageRoute( @@ -102,9 +87,7 @@ class _EditEntryScreen extends BasedState { if (res is String) { try { - var productMap = await apiClient.getProductByBarcode(res); - - var product = Product.fromJson(productMap); + var product = await client.product.getByBarcode(res); setState(() { products = [product]; @@ -128,7 +111,7 @@ class _EditEntryScreen extends BasedState { FTextInput( labelText: 'Product name', controller: productNameController, - onChanged: (_) => _getProducts(), + onChanged: (_) => getProducts(), autofocus: true, ), FTextInput( @@ -147,8 +130,7 @@ class _EditEntryScreen extends BasedState { context, MaterialPageRoute( builder: (context) => AddProductScreen( - apiClient: apiClient, - storage: storage, + ctx: ctx, ), ), ).then((product) { @@ -181,18 +163,18 @@ class _EditEntryScreen extends BasedState { mainAxisAlignment: MainAxisAlignment.end, children: [ FActionButton( - onPressed: _findProductByBarCode, + onPressed: findProductByBarCode, icon: Icons.photo_camera, ), const SizedBox(width: 10), FActionButton( - onPressed: _deleteEntry, + onPressed: deleteEntry, tag: "fap1", icon: Icons.delete, ), const SizedBox(width: 10), FActionButton( - onPressed: _saveEntry, + onPressed: saveEntry, tag: "fap2", icon: Icons.save, ), diff --git a/lib/screens/login.dart b/lib/screens/login.dart index 05c967d..74abcb3 100644 --- a/lib/screens/login.dart +++ b/lib/screens/login.dart @@ -7,8 +7,7 @@ import 'package:fooder/components/text.dart'; import 'package:fooder/components/button.dart'; class LoginScreen extends BasedScreen { - const LoginScreen( - {super.key, required super.apiClient, required super.storage}); + const LoginScreen({super.key, required super.ctx}); @override State createState() => _LoginScreen(); @@ -29,16 +28,15 @@ class _LoginScreen extends BasedState { Navigator.pushReplacement( context, MaterialPageRoute( - builder: (context) => - MainScreen(apiClient: apiClient, storage: storage), + builder: (context) => MainScreen(ctx: ctx), ), ); } // login client when button pressed - Future _login() async { + Future login() async { try { - await apiClient.login( + await client.api.login( usernameController.text, passwordController.text, ); @@ -60,18 +58,18 @@ class _LoginScreen extends BasedState { AutofillHints.password, ], }); - _asyncInitState().then((value) => null); + asyncInitState().then((value) => null); } - Future _asyncInitState() async { - await apiClient.loadToken(); + Future asyncInitState() async { + await client.api.loadToken(); - if (apiClient.refreshToken == null) { + if (client.api.refreshToken == null) { return; } try { - await apiClient.refresh(); + await client.api.refresh(); showText("Welcome back!"); popMeDaddy(); } on Exception catch (_) { @@ -107,13 +105,13 @@ class _LoginScreen extends BasedState { FTextInput( labelText: 'Password', controller: passwordController, - onFieldSubmitted: (_) => _login(), + onFieldSubmitted: (_) => login(), autofillHints: const [AutofillHints.password], obscureText: true, ), FButton( labelText: 'Sign In', - onPressed: _login, + onPressed: login, ), Padding( padding: const EdgeInsets.symmetric(vertical: 8), @@ -123,8 +121,7 @@ class _LoginScreen extends BasedState { context, MaterialPageRoute( builder: (context) => RegisterScreen( - apiClient: apiClient, - storage: storage, + ctx: ctx, ), ), ); diff --git a/lib/screens/main.dart b/lib/screens/main.dart index 544a66b..5c85d5c 100644 --- a/lib/screens/main.dart +++ b/lib/screens/main.dart @@ -10,8 +10,7 @@ import 'package:fooder/components/date_picker.dart'; import 'package:fooder/components/floating_action_button.dart'; class MainScreen extends BasedScreen { - const MainScreen( - {super.key, required super.apiClient, required super.storage}); + const MainScreen({super.key, required super.ctx}); @override State createState() => _MainScreen(); @@ -28,10 +27,10 @@ class _MainScreen extends BasedState { } Future _asyncInitState() async { - var diaryMap = await apiClient.getDiary(date: date); + var diary = await client.diary.get(date: date); setState(() { - diary = Diary.fromJson(diaryMap); + this.diary = diary; date = date; }); } @@ -52,8 +51,7 @@ class _MainScreen extends BasedState { await Navigator.push( context, MaterialPageRoute( - builder: (context) => AddEntryScreen( - apiClient: apiClient, diary: diary!, storage: storage), + builder: (context) => AddEntryScreen(ctx: ctx, diary: diary!), ), ).then((_) => _asyncInitState()); } @@ -67,9 +65,8 @@ class _MainScreen extends BasedState { context, MaterialPageRoute( builder: (context) => AddMealScreen( - apiClient: apiClient, + ctx: ctx, diary: diary!, - storage: storage, ), ), ).then((_) => _asyncInitState()); @@ -91,13 +88,11 @@ class _MainScreen extends BasedState { [ SummaryWidget( diary: diary!, - apiClient: apiClient, refreshParent: _asyncInitState, ), for (var (i, meal) in diary!.meals.indexed) MealWidget( - apiClient: apiClient, - storage: storage, + ctx: ctx, meal: meal, refreshParent: _asyncInitState, initiallyExpanded: i == 0, diff --git a/lib/screens/register.dart b/lib/screens/register.dart index d07f631..043273c 100644 --- a/lib/screens/register.dart +++ b/lib/screens/register.dart @@ -5,8 +5,7 @@ import 'package:fooder/components/text.dart'; import 'package:fooder/components/button.dart'; class RegisterScreen extends BasedScreen { - const RegisterScreen( - {super.key, required super.apiClient, required super.storage}); + const RegisterScreen({super.key, required super.ctx}); @override State createState() => _RegisterScreen(); @@ -50,7 +49,7 @@ class _RegisterScreen extends BasedState { } try { - await apiClient.register( + await client.api.register( usernameController.text, passwordController.text, ); diff --git a/lib/widgets/meal.dart b/lib/widgets/meal.dart index e0ed311..04607ff 100644 --- a/lib/widgets/meal.dart +++ b/lib/widgets/meal.dart @@ -3,8 +3,7 @@ import 'package:fooder/models/meal.dart'; import 'package:fooder/widgets/entry.dart'; import 'package:fooder/widgets/macro.dart'; import 'package:fooder/screens/edit_entry.dart'; -import 'package:fooder/client.dart'; -import 'package:fooder/storage.dart'; +import 'package:fooder/context.dart'; import 'dart:core'; class MealHeader extends StatelessWidget { @@ -38,8 +37,7 @@ class MealWidget extends StatelessWidget { static const maxWidth = 920.0; final Meal meal; - final ApiClient apiClient; - final Storage storage; + final Context ctx; final Function() refreshParent; final Function(String) showText; final bool initiallyExpanded; @@ -47,11 +45,10 @@ class MealWidget extends StatelessWidget { const MealWidget({ super.key, required this.meal, - required this.apiClient, + required this.ctx, required this.refreshParent, required this.initiallyExpanded, required this.showText, - required this.storage, }); Future saveMeal(context) async { @@ -77,7 +74,7 @@ class MealWidget extends StatelessWidget { IconButton( icon: const Icon(Icons.save), onPressed: () { - apiClient.saveMeal(meal, textFieldController.text); + ctx.client.meal.update(meal.id, textFieldController.text); Navigator.pop(context); showText("Meal saved"); }, @@ -88,11 +85,11 @@ class MealWidget extends StatelessWidget { ); } - Future _deleteMeal(Meal meal) async { - await apiClient.deleteMeal(meal.id); + Future deleteMeal(Meal meal) async { + await ctx.client.meal.delete(meal.id); } - Future deleteMeal(context) async { + Future deleteMealPopup(context) async { showDialog( context: context, barrierDismissible: false, @@ -109,7 +106,7 @@ class MealWidget extends StatelessWidget { IconButton( icon: const Icon(Icons.delete), onPressed: () { - _deleteMeal(meal).then((_) => refreshParent()); + deleteMeal(meal).then((_) => refreshParent()); Navigator.pop(context); showText("Meal deleted"); }, @@ -125,9 +122,8 @@ class MealWidget extends StatelessWidget { context, MaterialPageRoute( builder: (context) => EditEntryScreen( - apiClient: apiClient, + ctx: ctx, entry: entry, - storage: storage, ), ), ).then((_) => refreshParent()); @@ -195,7 +191,7 @@ class MealWidget extends StatelessWidget { ), IconButton( icon: const Icon(Icons.delete), - onPressed: () => deleteMeal(context), + onPressed: () => deleteMealPopup(context), ), ], ), diff --git a/lib/widgets/summary.dart b/lib/widgets/summary.dart index 30c53cc..82b6975 100644 --- a/lib/widgets/summary.dart +++ b/lib/widgets/summary.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:fooder/models/diary.dart'; import 'package:fooder/widgets/macro.dart'; -import 'package:fooder/client.dart'; import 'dart:core'; class SummaryHeader extends StatelessWidget { @@ -33,14 +32,10 @@ class SummaryWidget extends StatelessWidget { static const maxWidth = 920.0; final Diary diary; - final ApiClient apiClient; final Function() refreshParent; const SummaryWidget( - {super.key, - required this.diary, - required this.apiClient, - required this.refreshParent}); + {super.key, required this.diary, required this.refreshParent}); @override Widget build(BuildContext context) {