diff --git a/lib/client.dart b/lib/client.dart index 93d79e8..83b93df 100644 --- a/lib/client.dart +++ b/lib/client.dart @@ -14,10 +14,12 @@ class ApiClient { ApiClient({ required this.baseUrl, - }) { - () async { - await loadToken(); - }(); + }); + + static Future create({required String baseUrl}) async { + var client = ApiClient(baseUrl: baseUrl); + client.loadToken(); + return client; } Future loadToken() async { diff --git a/lib/client/based.dart b/lib/client/based.dart new file mode 100644 index 0000000..e0752d9 --- /dev/null +++ b/lib/client/based.dart @@ -0,0 +1 @@ +import 'package:fooder/client.dart'; diff --git a/lib/main.dart b/lib/main.dart index 534398a..cbd0746 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; import 'package:fooder/screens/login.dart'; import 'package:fooder/client.dart'; +import 'package:fooder/storage.dart'; import 'package:fooder/theme.dart'; class MyApp extends StatelessWidget { - const MyApp({super.key}); + final Storage storage; + final ApiClient apiClient; + + const MyApp({required this.storage, required this.apiClient, super.key}); @override Widget build(BuildContext context) { @@ -15,14 +19,18 @@ class MyApp extends StatelessWidget { themeMode: ThemeMode.system, debugShowCheckedModeBanner: false, home: LoginScreen( - apiClient: ApiClient( - baseUrl: 'https://fooderapi.domandoman.xyz/api', - ), + apiClient: apiClient, + storage: storage, ), ); } } -void main() { - runApp(const MyApp()); +void main() async { + var storage = await Storage.create(); + var apiClient = await ApiClient.create( + baseUrl: 'https://fooderapi.domandoman.xyz/api', + ); + + runApp(MyApp(storage: storage, apiClient: apiClient)); } diff --git a/lib/models/product.dart b/lib/models/product.dart index 1aeb3d3..7aab3ad 100644 --- a/lib/models/product.dart +++ b/lib/models/product.dart @@ -6,6 +6,7 @@ class Product { final double carb; final double fat; final double fiber; + final int usageCountCached; final String? barcode; Product( @@ -16,6 +17,7 @@ class Product { required this.carb, required this.fat, required this.fiber, + this.usageCountCached = 0, this.barcode}); Product.fromJson(Map map) @@ -26,6 +28,7 @@ class Product { carb = map['carb'] as double, fat = map['fat'] as double, fiber = map['fiber'] as double, + usageCountCached = map['usage_count_cached'] as int, barcode = map['barcode'] as String?; Map toMap() { @@ -38,6 +41,7 @@ class Product { 'fat': fat, 'fiber': fiber, 'barcode': barcode, + 'usage_count_cached': usageCountCached, }; } } diff --git a/lib/screens/add_entry.dart b/lib/screens/add_entry.dart index f57882b..abb0db1 100644 --- a/lib/screens/add_entry.dart +++ b/lib/screens/add_entry.dart @@ -9,14 +9,16 @@ import 'package:fooder/components/text.dart'; import 'package:fooder/components/dropdown.dart'; import 'package:fooder/components/floating_action_button.dart'; import 'package:fooder/screens/add_product.dart'; -import 'package:fooder/storage/base.dart'; import 'package:simple_barcode_scanner/simple_barcode_scanner.dart'; class AddEntryScreen extends BasedScreen { final Diary diary; const AddEntryScreen( - {super.key, required super.apiClient, required this.diary}); + {super.key, + required super.apiClient, + required super.storage, + required this.diary}); @override State createState() => _AddEntryScreen(); @@ -27,6 +29,7 @@ class _AddEntryScreen extends BasedState { final productNameController = TextEditingController(); Meal? meal; List products = []; + Diary get diary => widget.diary; @override void dispose() { @@ -45,35 +48,18 @@ class _AddEntryScreen extends BasedState { void initState() { super.initState(); setState(() { - meal = widget.diary.meals[0]; + meal = diary.meals[0]; }); _getProducts().then((value) => null); } Future _getProducts() async { - if (storage != null) { - var storagePorducts = await storage!.product.list(); - - if (storagePorducts.length > 5) { - print("Using local storage"); - setState(() { - products = storagePorducts; - }); - return; - } - } else { - print("No local storage"); - } - - var productsMap = - await widget.apiClient.getProducts(productNameController.text); + var productsMap = await apiClient.getProducts(productNameController.text); var parsedProducts = (productsMap['products'] as List) .map((e) => Product.fromJson(e as Map)) .toList(); - await storage!.product.bulkInsert(parsedProducts); - setState(() { products = parsedProducts; }); @@ -113,7 +99,7 @@ class _AddEntryScreen extends BasedState { return; } - await widget.apiClient.addEntry( + await apiClient.addEntry( grams: grams, productId: products[0].id, mealId: meal!.id, @@ -131,7 +117,7 @@ class _AddEntryScreen extends BasedState { if (res is String) { try { - var productMap = await widget.apiClient.getProductByBarcode(res); + var productMap = await apiClient.getProductByBarcode(res); var product = Product.fromJson(productMap); @@ -168,7 +154,7 @@ class _AddEntryScreen extends BasedState { }); }, items: >[ - for (var meal in widget.diary.meals) + for (var meal in diary.meals) DropdownMenuItem( value: meal, child: Text(meal.name), @@ -199,7 +185,8 @@ class _AddEntryScreen extends BasedState { context, MaterialPageRoute( builder: (context) => AddProductScreen( - apiClient: widget.apiClient, + apiClient: apiClient, + storage: storage, ), ), ).then((product) { diff --git a/lib/screens/add_meal.dart b/lib/screens/add_meal.dart index 4de8edc..cf14e70 100644 --- a/lib/screens/add_meal.dart +++ b/lib/screens/add_meal.dart @@ -10,7 +10,10 @@ class AddMealScreen extends BasedScreen { final Diary diary; const AddMealScreen( - {super.key, required super.apiClient, required this.diary}); + {super.key, + required super.apiClient, + required super.storage, + required this.diary}); @override State createState() => _AddMealScreen(); @@ -22,10 +25,10 @@ class _AddMealScreen extends BasedState { bool nameChanged = false; List presets = []; Preset? selectedPreset; + Diary get diary => widget.diary; Future _getPresets() async { - var presetsMap = - await widget.apiClient.getPresets(presetNameController.text); + var presetsMap = await apiClient.getPresets(presetNameController.text); setState(() { presets = (presetsMap['presets'] as List) @@ -39,7 +42,7 @@ class _AddMealScreen extends BasedState { void initState() { super.initState(); setState(() { - nameController.text = "Meal ${widget.diary.meals.length + 1}"; + nameController.text = "Meal ${diary.meals.length + 1}"; }); _getPresets(); } @@ -57,15 +60,15 @@ class _AddMealScreen extends BasedState { } Future _addMeal() async { - await widget.apiClient.addMeal( + await apiClient.addMeal( name: nameController.text, - diaryId: widget.diary.id, + diaryId: diary.id, ); popMeDaddy(); } Future _deletePreset(Preset preset) async { - widget.apiClient.deletePreset(preset.id); + apiClient.deletePreset(preset.id); setState(() { presets.remove(preset); }); @@ -103,9 +106,9 @@ class _AddMealScreen extends BasedState { return; } - await widget.apiClient.addMealFromPreset( + await apiClient.addMealFromPreset( name: nameChanged ? nameController.text : selectedPreset!.name, - diaryId: widget.diary.id, + diaryId: diary.id, presetId: selectedPreset!.id, ); popMeDaddy(); diff --git a/lib/screens/add_product.dart b/lib/screens/add_product.dart index 944c7ab..4efa4cc 100644 --- a/lib/screens/add_product.dart +++ b/lib/screens/add_product.dart @@ -7,7 +7,8 @@ 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}); + const AddProductScreen( + {super.key, required super.apiClient, required super.storage}); @override State createState() => _AddProductScreen(); @@ -61,7 +62,7 @@ class _AddProductScreen extends BasedState { } try { - var productJson = await widget.apiClient.addProduct( + var productJson = await apiClient.addProduct( carb: carb, fat: fat, protein: protein, diff --git a/lib/screens/based.dart b/lib/screens/based.dart index 995c98d..b157b2a 100644 --- a/lib/screens/based.dart +++ b/lib/screens/based.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:fooder/client.dart'; +import 'package:fooder/storage.dart'; import 'package:fooder/components/app_bar.dart'; import 'package:fooder/components/navigation_bar.dart'; import 'package:fooder/screens/login.dart'; import 'package:fooder/screens/main.dart'; -import 'package:fooder/storage/base.dart'; TextStyle logoStyle(context) { return Theme.of(context).textTheme.labelLarge!.copyWith( @@ -14,34 +14,27 @@ TextStyle logoStyle(context) { abstract class BasedScreen extends StatefulWidget { final ApiClient apiClient; - final Storage? storage; + final Storage storage; - const BasedScreen({super.key, required this.apiClient, this.storage}); + const BasedScreen( + {super.key, required this.apiClient, required this.storage}); } abstract class BasedState extends State { - Storage? storage; + ApiClient get apiClient => widget.apiClient; + Storage get storage => widget.storage; - @override - void initState() { - super.initState(); - _asyncInitState().then((value) => null); + void logout() async { + await apiClient.logout(); + backToLogin(); } - Future _asyncInitState() async { - if (widget.storage != null) { - this.storage = widget.storage; - } else { - this.storage = await Storage.create(); - } - } - - void _logout() async { - await widget.apiClient.logout(); + void backToLogin() { Navigator.pushReplacement( context, MaterialPageRoute( - builder: (context) => LoginScreen(apiClient: widget.apiClient), + builder: (context) => + LoginScreen(apiClient: apiClient, storage: storage), ), ); } @@ -50,7 +43,8 @@ abstract class BasedState extends State { Navigator.pushReplacement( context, MaterialPageRoute( - builder: (context) => MainScreen(apiClient: widget.apiClient), + builder: (context) => + MainScreen(apiClient: apiClient, storage: storage), ), ); } @@ -63,7 +57,7 @@ abstract class BasedState extends State { Icons.logout, color: Theme.of(context).colorScheme.onSurfaceVariant, ), - onPressed: _logout, + onPressed: logout, ), ], ); diff --git a/lib/screens/edit_entry.dart b/lib/screens/edit_entry.dart index 3eb4333..3db6ece 100644 --- a/lib/screens/edit_entry.dart +++ b/lib/screens/edit_entry.dart @@ -13,7 +13,10 @@ class EditEntryScreen extends BasedScreen { final Entry entry; const EditEntryScreen( - {super.key, required super.apiClient, required this.entry}); + {super.key, + required super.apiClient, + required super.storage, + required this.entry}); @override State createState() => _EditEntryScreen(); @@ -23,6 +26,7 @@ class _EditEntryScreen extends BasedState { final gramsController = TextEditingController(); final productNameController = TextEditingController(); List products = []; + Entry get entry => entry; @override void dispose() { @@ -39,15 +43,14 @@ class _EditEntryScreen extends BasedState { void initState() { super.initState(); setState(() { - gramsController.text = widget.entry.grams.toString(); - productNameController.text = widget.entry.product.name; - products = [widget.entry.product]; + gramsController.text = entry.grams.toString(); + productNameController.text = entry.product.name; + products = [entry.product]; }); } Future _getProducts() async { - var productsMap = - await widget.apiClient.getProducts(productNameController.text); + var productsMap = await apiClient.getProducts(productNameController.text); setState(() { products = (productsMap['products'] as List) .map((e) => Product.fromJson(e as Map)) @@ -75,17 +78,17 @@ class _EditEntryScreen extends BasedState { return; } - await widget.apiClient.updateEntry( - widget.entry.id, + await apiClient.updateEntry( + entry.id, grams: grams, productId: products[0].id, - mealId: widget.entry.mealId, + mealId: entry.mealId, ); popMeDaddy(); } Future _deleteEntry() async { - await widget.apiClient.deleteEntry(widget.entry.id); + await apiClient.deleteEntry(widget.entry.id); popMeDaddy(); } @@ -99,7 +102,7 @@ class _EditEntryScreen extends BasedState { if (res is String) { try { - var productMap = await widget.apiClient.getProductByBarcode(res); + var productMap = await apiClient.getProductByBarcode(res); var product = Product.fromJson(productMap); @@ -144,7 +147,8 @@ class _EditEntryScreen extends BasedState { context, MaterialPageRoute( builder: (context) => AddProductScreen( - apiClient: widget.apiClient, + apiClient: apiClient, + storage: storage, ), ), ).then((product) { diff --git a/lib/screens/login.dart b/lib/screens/login.dart index e7958e8..05c967d 100644 --- a/lib/screens/login.dart +++ b/lib/screens/login.dart @@ -7,7 +7,8 @@ import 'package:fooder/components/text.dart'; import 'package:fooder/components/button.dart'; class LoginScreen extends BasedScreen { - const LoginScreen({super.key, required super.apiClient}); + const LoginScreen( + {super.key, required super.apiClient, required super.storage}); @override State createState() => _LoginScreen(); @@ -28,7 +29,8 @@ class _LoginScreen extends BasedState { Navigator.pushReplacement( context, MaterialPageRoute( - builder: (context) => MainScreen(apiClient: widget.apiClient), + builder: (context) => + MainScreen(apiClient: apiClient, storage: storage), ), ); } @@ -36,7 +38,7 @@ class _LoginScreen extends BasedState { // login client when button pressed Future _login() async { try { - await widget.apiClient.login( + await apiClient.login( usernameController.text, passwordController.text, ); @@ -62,14 +64,14 @@ class _LoginScreen extends BasedState { } Future _asyncInitState() async { - await widget.apiClient.loadToken(); + await apiClient.loadToken(); - if (widget.apiClient.refreshToken == null) { + if (apiClient.refreshToken == null) { return; } try { - await widget.apiClient.refresh(); + await apiClient.refresh(); showText("Welcome back!"); popMeDaddy(); } on Exception catch (_) { @@ -120,8 +122,10 @@ class _LoginScreen extends BasedState { Navigator.push( context, MaterialPageRoute( - builder: (context) => - RegisterScreen(apiClient: widget.apiClient), + builder: (context) => RegisterScreen( + apiClient: apiClient, + storage: storage, + ), ), ); }, diff --git a/lib/screens/main.dart b/lib/screens/main.dart index d970084..544a66b 100644 --- a/lib/screens/main.dart +++ b/lib/screens/main.dart @@ -8,10 +8,10 @@ import 'package:fooder/widgets/meal.dart'; import 'package:fooder/components/sliver.dart'; import 'package:fooder/components/date_picker.dart'; import 'package:fooder/components/floating_action_button.dart'; -import 'package:fooder/storage/base.dart'; class MainScreen extends BasedScreen { - const MainScreen({super.key, required super.apiClient}); + const MainScreen( + {super.key, required super.apiClient, required super.storage}); @override State createState() => _MainScreen(); @@ -28,7 +28,7 @@ class _MainScreen extends BasedState { } Future _asyncInitState() async { - var diaryMap = await widget.apiClient.getDiary(date: date); + var diaryMap = await apiClient.getDiary(date: date); setState(() { diary = Diary.fromJson(diaryMap); @@ -52,8 +52,8 @@ class _MainScreen extends BasedState { await Navigator.push( context, MaterialPageRoute( - builder: (context) => - AddEntryScreen(apiClient: widget.apiClient, diary: diary!), + builder: (context) => AddEntryScreen( + apiClient: apiClient, diary: diary!, storage: storage), ), ).then((_) => _asyncInitState()); } @@ -67,8 +67,9 @@ class _MainScreen extends BasedState { context, MaterialPageRoute( builder: (context) => AddMealScreen( - apiClient: widget.apiClient, + apiClient: apiClient, diary: diary!, + storage: storage, ), ), ).then((_) => _asyncInitState()); @@ -90,13 +91,14 @@ class _MainScreen extends BasedState { [ SummaryWidget( diary: diary!, - apiClient: widget.apiClient, + apiClient: apiClient, refreshParent: _asyncInitState, ), for (var (i, meal) in diary!.meals.indexed) MealWidget( + apiClient: apiClient, + storage: storage, meal: meal, - apiClient: widget.apiClient, refreshParent: _asyncInitState, initiallyExpanded: i == 0, showText: showText, diff --git a/lib/screens/register.dart b/lib/screens/register.dart index 9492c8e..d07f631 100644 --- a/lib/screens/register.dart +++ b/lib/screens/register.dart @@ -5,7 +5,8 @@ import 'package:fooder/components/text.dart'; import 'package:fooder/components/button.dart'; class RegisterScreen extends BasedScreen { - const RegisterScreen({super.key, required super.apiClient}); + const RegisterScreen( + {super.key, required super.apiClient, required super.storage}); @override State createState() => _RegisterScreen(); @@ -49,7 +50,7 @@ class _RegisterScreen extends BasedState { } try { - await widget.apiClient.register( + await apiClient.register( usernameController.text, passwordController.text, ); diff --git a/lib/storage/base.dart b/lib/storage.dart similarity index 76% rename from lib/storage/base.dart rename to lib/storage.dart index 75c6249..2c8ed38 100644 --- a/lib/storage/base.dart +++ b/lib/storage.dart @@ -9,23 +9,23 @@ class Storage { Database db; ProductStorage product; + static const String path = "storage.db"; + Storage({required this.db, required this.product}); static Future create() async { - var db = await database(); - return Storage(db: db, product: ProductStorage(db: db)); - } - - static Future createTables(Database db, int version) async { - await ProductStorage.createTable(db, version); - } - - static Future database({String path = "storage.db"}) async { WidgetsFlutterBinding.ensureInitialized(); - return openDatabase( + var db = await openDatabase( join(await getDatabasesPath(), path), onCreate: createTables, version: 1, ); + return Storage(db: db, product: ProductStorage(db: db)); + } + + static Future createTables(Database db, int version) async { + var batch = db.batch(); + await ProductStorage.createTable(batch); + await batch.commit(noResult: true); } } diff --git a/lib/storage/based.dart b/lib/storage/based.dart new file mode 100644 index 0000000..5efa2fe --- /dev/null +++ b/lib/storage/based.dart @@ -0,0 +1,11 @@ +import 'dart:async'; + +import 'package:sqflite/sqflite.dart'; + +abstract class StorageBased { + Database db; + + StorageBased({required this.db}); + + static Future createTable(Batch batch) async {} +} diff --git a/lib/storage/product.dart b/lib/storage/product.dart index 96a0122..33c2f13 100644 --- a/lib/storage/product.dart +++ b/lib/storage/product.dart @@ -2,29 +2,32 @@ import 'dart:async'; import 'package:sqflite/sqflite.dart'; import 'package:fooder/models/product.dart'; +import 'package:fooder/storage/based.dart'; -class ProductStorage { - Database db; +class ProductStorage extends StorageBased { + ProductStorage({required super.db}); - ProductStorage({required this.db}); - - static Future createTable(Database db, int version) async { - await db.execute(''' + static Future createTable(Batch batch) async { + batch.execute(''' CREATE TABLE product( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, + id INTEGER, + name TEXT, barcode TEXT, - calories NOT NULL, - protein REAL NOT NULL, - carb REAL NOT NULL, - fat REAL NOT NULL, - fiber REAL NOT NULL + calories REAL, + protein REAL, + carb REAL, + fat REAL, + fiber REAL, + usage_count_cached INTEGER ) '''); } - Future> list() async { - var result = await db.query('product'); + Future> 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(); } diff --git a/lib/widgets/meal.dart b/lib/widgets/meal.dart index 24430c8..e0ed311 100644 --- a/lib/widgets/meal.dart +++ b/lib/widgets/meal.dart @@ -4,6 +4,7 @@ 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 'dart:core'; class MealHeader extends StatelessWidget { @@ -38,6 +39,7 @@ class MealWidget extends StatelessWidget { final Meal meal; final ApiClient apiClient; + final Storage storage; final Function() refreshParent; final Function(String) showText; final bool initiallyExpanded; @@ -49,6 +51,7 @@ class MealWidget extends StatelessWidget { required this.refreshParent, required this.initiallyExpanded, required this.showText, + required this.storage, }); Future saveMeal(context) async { @@ -124,6 +127,7 @@ class MealWidget extends StatelessWidget { builder: (context) => EditEntryScreen( apiClient: apiClient, entry: entry, + storage: storage, ), ), ).then((_) => refreshParent());