[context] refactor screens etc to use global context storing client and storage, for easier further changes'

This commit is contained in:
Piotr Domański 2024-08-04 20:14:14 +02:00
parent 07c92443ce
commit 33fe0eff39
20 changed files with 521 additions and 488 deletions

View file

@ -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<Client> create({required String baseUrl}) async {
var api = await ApiClient.create(baseUrl: 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<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,
});
return Client(
api: api,
product: ProductClient(apiClient: api),
preset: PresetClient(apiClient: api),
meal: MealClient(apiClient: api),
diary: DiaryClient(apiClient: api),
entry: EntryClient(apiClient: api));
}
}

219
lib/client/api.dart Normal file
View 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");
}
}
}

View file

@ -1 +1,7 @@
import 'package:fooder/client.dart';
import 'package:fooder/client/api.dart';
abstract class BasedClient {
final ApiClient apiClient;
const BasedClient({required this.apiClient});
}

18
lib/client/diary.dart Normal file
View 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
View 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
View 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
View 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
View 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
View 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);
}
}

View file

@ -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));
}

View file

@ -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<AddEntryScreen> createState() => _AddEntryScreen();
@ -54,14 +50,10 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
}
Future<void> _getProducts() async {
var productsMap = await apiClient.getProducts(productNameController.text);
var parsedProducts = (productsMap['products'] as List<dynamic>)
.map((e) => Product.fromJson(e as Map<String, dynamic>))
.toList();
var products = await client.product.list(productNameController.text);
setState(() {
products = parsedProducts;
this.products = products;
});
}
@ -99,7 +91,7 @@ class _AddEntryScreen extends BasedState<AddEntryScreen> {
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<AddEntryScreen> {
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<AddEntryScreen> {
context,
MaterialPageRoute(
builder: (context) => AddProductScreen(
apiClient: apiClient,
storage: storage,
ctx: ctx,
),
),
).then((product) {

View file

@ -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<AddMealScreen> createState() => _AddMealScreen();
@ -28,12 +24,10 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
Diary get diary => widget.diary;
Future<void> _getPresets() async {
var presetsMap = await apiClient.getPresets(presetNameController.text);
var presets = await client.preset.list(presetNameController.text);
setState(() {
presets = (presetsMap['presets'] as List<dynamic>)
.map((e) => Preset.fromJson(e as Map<String, dynamic>))
.toList();
this.presets = presets;
selectedPreset = null;
});
}
@ -59,22 +53,22 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
);
}
Future<void> _addMeal() async {
await apiClient.addMeal(
Future<void> addMeal() async {
await client.meal.create(
name: nameController.text,
diaryId: diary.id,
);
popMeDaddy();
}
Future<void> _deletePreset(Preset preset) async {
apiClient.deletePreset(preset.id);
Future<void> deletePreset(Preset preset) async {
client.preset.delete(preset.id);
setState(() {
presets.remove(preset);
});
}
Future<void> deletePreset(context, Preset preset) async {
Future<void> deletePresetPopup(context, Preset preset) async {
showDialog(
context: context,
builder: (context) {
@ -90,7 +84,7 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
_deletePreset(preset);
deletePreset(preset);
Navigator.pop(context);
},
),
@ -100,13 +94,13 @@ class _AddMealScreen extends BasedState<AddMealScreen> {
);
}
Future<void> _addMealFromPreset() async {
Future<void> 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<AddMealScreen> {
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<AddMealScreen> {
),
),
floatingActionButton: FActionButton(
onPressed: _addMealFromPreset,
onPressed: addMealFromPreset,
icon: Icons.playlist_add_rounded,
),
);

View file

@ -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<AddProductScreen> createState() => _AddProductScreen();
@ -38,38 +37,25 @@ class _AddProductScreen extends BasedState<AddProductScreen> {
);
}
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;
}
}
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");
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 =
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<AddProductScreen> {
])),
),
floatingActionButton: FActionButton(
onPressed: _addProduct,
onPressed: addProduct,
icon: Icons.save,
),
);

View file

@ -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<T extends BasedScreen> extends State<T> {
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<T extends BasedScreen> extends State<T> {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) =>
LoginScreen(apiClient: apiClient, storage: storage),
builder: (context) => LoginScreen(ctx: ctx),
),
);
}
@ -43,8 +42,7 @@ abstract class BasedState<T extends BasedScreen> extends State<T> {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) =>
MainScreen(apiClient: apiClient, storage: storage),
builder: (context) => MainScreen(ctx: ctx),
),
);
}
@ -103,6 +101,18 @@ abstract class BasedState<T extends BasedScreen> extends State<T> {
);
}
Future<double?> parseDouble(String text, String name,
{bool silent = false}) async {
try {
return double.parse(text.replaceAll(",", "."));
} catch (e) {
if (!silent) {
showError("$name must be a number");
}
return null;
}
}
void showError(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(

View file

@ -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<EditEntryScreen> createState() => _EditEntryScreen();
@ -26,7 +22,7 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
final gramsController = TextEditingController();
final productNameController = TextEditingController();
List<Product> products = [];
Entry get entry => entry;
Entry get entry => widget.entry;
@override
void dispose() {
@ -49,36 +45,25 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
});
}
Future<void> _getProducts() async {
var productsMap = await apiClient.getProducts(productNameController.text);
Future<void> getProducts() async {
var products = await client.product.list(productNameController.text);
setState(() {
products = (productsMap['products'] as List<dynamic>)
.map((e) => Product.fromJson(e as Map<String, dynamic>))
.toList();
this.products = products;
});
}
Future<double?> _parseDouble(String text, String name) async {
try {
return double.parse(text.replaceAll(",", "."));
} catch (e) {
showError("$name must be a number");
return null;
}
}
Future<void> _saveEntry() async {
Future<void> 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<EditEntryScreen> {
popMeDaddy();
}
Future<void> _deleteEntry() async {
await apiClient.deleteEntry(widget.entry.id);
Future<void> deleteEntry() async {
await client.entry.delete(widget.entry.id);
popMeDaddy();
}
Future<void> _findProductByBarCode() async {
Future<void> findProductByBarCode() async {
var res = await Navigator.push(
context,
MaterialPageRoute(
@ -102,9 +87,7 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
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<EditEntryScreen> {
FTextInput(
labelText: 'Product name',
controller: productNameController,
onChanged: (_) => _getProducts(),
onChanged: (_) => getProducts(),
autofocus: true,
),
FTextInput(
@ -147,8 +130,7 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
context,
MaterialPageRoute(
builder: (context) => AddProductScreen(
apiClient: apiClient,
storage: storage,
ctx: ctx,
),
),
).then((product) {
@ -181,18 +163,18 @@ class _EditEntryScreen extends BasedState<EditEntryScreen> {
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
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,
),

View file

@ -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<LoginScreen> createState() => _LoginScreen();
@ -29,16 +28,15 @@ class _LoginScreen extends BasedState<LoginScreen> {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) =>
MainScreen(apiClient: apiClient, storage: storage),
builder: (context) => MainScreen(ctx: ctx),
),
);
}
// login client when button pressed
Future<void> _login() async {
Future<void> login() async {
try {
await apiClient.login(
await client.api.login(
usernameController.text,
passwordController.text,
);
@ -60,18 +58,18 @@ class _LoginScreen extends BasedState<LoginScreen> {
AutofillHints.password,
],
});
_asyncInitState().then((value) => null);
asyncInitState().then((value) => null);
}
Future<void> _asyncInitState() async {
await apiClient.loadToken();
Future<void> 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<LoginScreen> {
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<LoginScreen> {
context,
MaterialPageRoute(
builder: (context) => RegisterScreen(
apiClient: apiClient,
storage: storage,
ctx: ctx,
),
),
);

View file

@ -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<MainScreen> createState() => _MainScreen();
@ -28,10 +27,10 @@ class _MainScreen extends BasedState<MainScreen> {
}
Future<void> _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<MainScreen> {
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<MainScreen> {
context,
MaterialPageRoute(
builder: (context) => AddMealScreen(
apiClient: apiClient,
ctx: ctx,
diary: diary!,
storage: storage,
),
),
).then((_) => _asyncInitState());
@ -91,13 +88,11 @@ class _MainScreen extends BasedState<MainScreen> {
[
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,

View file

@ -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<RegisterScreen> createState() => _RegisterScreen();
@ -50,7 +49,7 @@ class _RegisterScreen extends BasedState<RegisterScreen> {
}
try {
await apiClient.register(
await client.api.register(
usernameController.text,
passwordController.text,
);

View file

@ -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<void> 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<void> _deleteMeal(Meal meal) async {
await apiClient.deleteMeal(meal.id);
Future<void> deleteMeal(Meal meal) async {
await ctx.client.meal.delete(meal.id);
}
Future<void> deleteMeal(context) async {
Future<void> 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),
),
],
),

View file

@ -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) {