From 643861bf1e781dfe3bef0ad3768af5bfc83b9349 Mon Sep 17 00:00:00 2001 From: Avior Date: Sat, 7 Jan 2023 22:11:20 +0100 Subject: [PATCH] feat: Add support for Food input --- .../2.json | 12 +- .../3.json | 232 ++++++++++++++++++ .../res/drawable/baseline_chevron_left_24.xml | 5 + .../drawable/baseline_chevron_right_24.xml | 5 + .../dzeio/openhealth/adapters/FoodAdapter.kt | 10 +- .../com/dzeio/openhealth/data/AppDatabase.kt | 13 +- .../com/dzeio/openhealth/data/food/Food.kt | 36 ++- .../openhealth/data/food/FoodRepository.kt | 8 + .../data/openfoodfact/OFFNutriments.kt | 4 +- .../data/openfoodfact/OFFProduct.kt | 16 +- .../data/openfoodfact/OpenFoodFactService.kt | 21 +- .../dzeio/openhealth/ui/food/FoodDialog.kt | 88 +++++++ .../openhealth/ui/food/FoodDialogViewModel.kt | 55 +++++ .../openhealth/ui/food/FoodHomeFragment.kt | 77 +++++- .../openhealth/ui/food/FoodHomeViewModel.kt | 47 +++- .../openhealth/ui/food/SearchFoodDialog.kt | 81 ++++++ .../ui/food/SearchFoodDialogViewModel.kt | 37 +++ .../dzeio/openhealth/ui/home/HomeFragment.kt | 74 +++--- .../dzeio/openhealth/ui/home/HomeViewModel.kt | 16 ++ .../openhealth/utils/DownloadImageTask.kt | 30 +++ .../drawable/ic_baseline_chevron_left_24.xml | 5 + .../drawable/ic_baseline_chevron_right_24.xml | 5 + .../main/res/drawable/outline_fastfood_24.xml | 5 + .../main/res/layout/dialog_food_product.xml | 154 ++++++++++++ .../res/layout/dialog_food_search_product.xml | 30 +++ .../main/res/layout/fragment_food_home.xml | 146 ++++++++++- app/src/main/res/layout/fragment_home.xml | 15 +- app/src/main/res/layout/item_food.xml | 13 +- .../main/res/navigation/mobile_navigation.xml | 31 ++- app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 31 files changed, 1194 insertions(+), 81 deletions(-) create mode 100644 app/schemas/com.dzeio.openhealth.data.AppDatabase/3.json create mode 100644 app/src/debug/res/drawable/baseline_chevron_left_24.xml create mode 100644 app/src/debug/res/drawable/baseline_chevron_right_24.xml create mode 100644 app/src/main/java/com/dzeio/openhealth/ui/food/FoodDialog.kt create mode 100644 app/src/main/java/com/dzeio/openhealth/ui/food/FoodDialogViewModel.kt create mode 100644 app/src/main/java/com/dzeio/openhealth/ui/food/SearchFoodDialog.kt create mode 100644 app/src/main/java/com/dzeio/openhealth/ui/food/SearchFoodDialogViewModel.kt create mode 100644 app/src/main/java/com/dzeio/openhealth/utils/DownloadImageTask.kt create mode 100644 app/src/main/res/drawable/ic_baseline_chevron_left_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_chevron_right_24.xml create mode 100644 app/src/main/res/drawable/outline_fastfood_24.xml create mode 100644 app/src/main/res/layout/dialog_food_product.xml create mode 100644 app/src/main/res/layout/dialog_food_search_product.xml diff --git a/app/schemas/com.dzeio.openhealth.data.AppDatabase/2.json b/app/schemas/com.dzeio.openhealth.data.AppDatabase/2.json index 59c704e..06863c9 100644 --- a/app/schemas/com.dzeio.openhealth.data.AppDatabase/2.json +++ b/app/schemas/com.dzeio.openhealth.data.AppDatabase/2.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 2, - "identityHash": "794ed5ee15db239f9a2708b951f55552", + "identityHash": "0f92ae44f4503b964d4986959a15ef4e", "entities": [ { "tableName": "Weight", @@ -150,7 +150,7 @@ }, { "tableName": "Food", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `quantity` INTEGER NOT NULL, `proteins` REAL NOT NULL, `carbohydrates` REAL NOT NULL, `fat` REAL NOT NULL, `energy` REAL NOT NULL, `timestamp` INTEGER NOT NULL)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `serving` TEXT NOT NULL, `quantity` INTEGER NOT NULL, `proteins` REAL NOT NULL, `carbohydrates` REAL NOT NULL, `fat` REAL NOT NULL, `energy` REAL NOT NULL, `timestamp` INTEGER NOT NULL)", "fields": [ { "fieldPath": "id", @@ -164,6 +164,12 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "serving", + "columnName": "serving", + "affinity": "TEXT", + "notNull": true + }, { "fieldPath": "quantity", "columnName": "quantity", @@ -214,7 +220,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '794ed5ee15db239f9a2708b951f55552')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0f92ae44f4503b964d4986959a15ef4e')" ] } } \ No newline at end of file diff --git a/app/schemas/com.dzeio.openhealth.data.AppDatabase/3.json b/app/schemas/com.dzeio.openhealth.data.AppDatabase/3.json new file mode 100644 index 0000000..73f0509 --- /dev/null +++ b/app/schemas/com.dzeio.openhealth.data.AppDatabase/3.json @@ -0,0 +1,232 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "414712cc283c7f1d14cde8e00da277fb", + "entities": [ + { + "tableName": "Weight", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `weight` REAL NOT NULL, `timestamp` INTEGER NOT NULL, `source` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Weight_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Weight_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Water", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `value` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `source` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Water_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Water_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Step", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `value` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `source` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Step_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Step_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Food", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `serving` TEXT NOT NULL, `quantity` REAL NOT NULL, `proteins` REAL NOT NULL, `carbohydrates` REAL NOT NULL, `fat` REAL NOT NULL, `energy` REAL NOT NULL, `image` TEXT, `timestamp` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serving", + "columnName": "serving", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "quantity", + "columnName": "quantity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "proteins", + "columnName": "proteins", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "carbohydrates", + "columnName": "carbohydrates", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "fat", + "columnName": "fat", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "energy", + "columnName": "energy", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '414712cc283c7f1d14cde8e00da277fb')" + ] + } +} \ No newline at end of file diff --git a/app/src/debug/res/drawable/baseline_chevron_left_24.xml b/app/src/debug/res/drawable/baseline_chevron_left_24.xml new file mode 100644 index 0000000..25a728b --- /dev/null +++ b/app/src/debug/res/drawable/baseline_chevron_left_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/debug/res/drawable/baseline_chevron_right_24.xml b/app/src/debug/res/drawable/baseline_chevron_right_24.xml new file mode 100644 index 0000000..e7cf886 --- /dev/null +++ b/app/src/debug/res/drawable/baseline_chevron_right_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/java/com/dzeio/openhealth/adapters/FoodAdapter.kt b/app/src/main/java/com/dzeio/openhealth/adapters/FoodAdapter.kt index f091dc5..1d14d25 100644 --- a/app/src/main/java/com/dzeio/openhealth/adapters/FoodAdapter.kt +++ b/app/src/main/java/com/dzeio/openhealth/adapters/FoodAdapter.kt @@ -6,8 +6,11 @@ import com.dzeio.openhealth.core.BaseAdapter import com.dzeio.openhealth.core.BaseViewHolder import com.dzeio.openhealth.data.food.Food import com.dzeio.openhealth.databinding.ItemFoodBinding +import com.dzeio.openhealth.utils.DownloadImageTask +import kotlin.math.roundToInt -class FoodAdapter() : BaseAdapter() { + +class FoodAdapter : BaseAdapter() { override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> ItemFoodBinding get() = ItemFoodBinding::inflate @@ -19,8 +22,9 @@ class FoodAdapter() : BaseAdapter() { item: Food, position: Int ) { - holder.binding.foodName.text = item.name - holder.binding.foodDescription.text = item.energy.toString() + DownloadImageTask(holder.binding.productImage).execute(item.image) + holder.binding.foodName.text = "${item.name}" + holder.binding.foodDescription.text = "${item.quantity.roundToInt()}${item.serving.replace(Regex("\\d+"), "")} (${(item.energy / 100 * item.quantity).roundToInt()} kcal)" holder.binding.edit.setOnClickListener { onItemClick?.invoke(item) } diff --git a/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt b/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt index b0fb3dc..055fba6 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt @@ -5,6 +5,8 @@ import androidx.room.AutoMigration import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import com.dzeio.openhealth.data.food.Food import com.dzeio.openhealth.data.food.FoodDao import com.dzeio.openhealth.data.step.Step @@ -21,7 +23,7 @@ import com.dzeio.openhealth.data.weight.WeightDao Step::class, Food::class ], - version = 2, + version = 3, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2) @@ -54,6 +56,7 @@ abstract class AppDatabase : RoomDatabase() { // https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785 private fun buildDatabase(context: Context): AppDatabase { return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) + .addMigrations(MIGRATION_2_3) // .addCallback(object : Callback() { // override fun onCreate(db: SupportSQLiteDatabase) { // super.onCreate(db) @@ -66,5 +69,13 @@ abstract class AppDatabase : RoomDatabase() { // }) .build() } + + private val MIGRATION_2_3 = object : Migration(2, 3) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE Food ADD COLUMN serving TEXT NOT NULL") + database.execSQL("ALTER TABLE Food ADD COLUMN image TEXT") + database.execSQL("ALTER TABLE Food ") + } + } } } diff --git a/app/src/main/java/com/dzeio/openhealth/data/food/Food.kt b/app/src/main/java/com/dzeio/openhealth/data/food/Food.kt index 896f547..bfda863 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/food/Food.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/food/Food.kt @@ -2,6 +2,7 @@ package com.dzeio.openhealth.data.food import androidx.room.Entity import androidx.room.PrimaryKey +import com.dzeio.openhealth.data.openfoodfact.OFFProduct import java.util.Calendar import java.util.TimeZone @@ -10,11 +11,40 @@ data class Food( @PrimaryKey(autoGenerate = true) var id: Long = 0, var name: String, - var quantity: Int, + var serving: String, + var quantity: Float, var proteins: Float, var carbohydrates: Float, var fat: Float, var energy: Float, - var timestamp: Long = Calendar.getInstance(TimeZone.getTimeZone("UTC")).timeInMillis -) + /** + * the url of the image + */ + var image: String?, + + var timestamp: Long = Calendar.getInstance(TimeZone.getTimeZone("UTC")).timeInMillis, +) { + companion object { + fun fromOpenFoodFact(food: OFFProduct, quantity: Float? = null): Food { + var eaten = quantity ?: food.servingQuantity ?: food.productQuantity ?: 0f + if (eaten == 0f) { + if (food.servingQuantity != null && food.servingQuantity != 0f) { + eaten = food.servingQuantity!! + } else if (food.productQuantity != null && food.productQuantity != 0f) { + eaten = food.productQuantity!! + } + } + return Food( + name = food.name, + serving = (food.servingSize ?: food.quantity ?: "unknown").replace(Regex(" +"), ""), + quantity = eaten, + proteins = food.nutriments.proteins, + carbohydrates = food.nutriments.carbohydrates, + fat = food.nutriments.fat, + energy = food.nutriments.energy ?: (food.nutriments.energyKJ * 0.2390057361).toFloat(), + image = food.image + ) + } + } +} diff --git a/app/src/main/java/com/dzeio/openhealth/data/food/FoodRepository.kt b/app/src/main/java/com/dzeio/openhealth/data/food/FoodRepository.kt index a7e0fe3..c00ca7c 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/food/FoodRepository.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/food/FoodRepository.kt @@ -16,4 +16,12 @@ class FoodRepository @Inject constructor( fun getAll() = dao.getAll() + suspend fun add(food: Food) = dao.insert(food) + + fun getById(id: Long) = dao.getOne(id) + + suspend fun delete(food: Food) = dao.delete(food) + + suspend fun update(food: Food) = dao.update(food) + } diff --git a/app/src/main/java/com/dzeio/openhealth/data/openfoodfact/OFFNutriments.kt b/app/src/main/java/com/dzeio/openhealth/data/openfoodfact/OFFNutriments.kt index 0e6ad45..a561356 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/openfoodfact/OFFNutriments.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/openfoodfact/OFFNutriments.kt @@ -6,7 +6,9 @@ data class OFFNutriments( @SerializedName("carbohydrates_100g") var carbohydrates: Float, @SerializedName("energy-kcal_100g") - var energy: Float, + var energy: Float?, + @SerializedName("energy-kj_100g") + var energyKJ: Float, @SerializedName("fat_100g") var fat: Float, @SerializedName("proteins_100g") diff --git a/app/src/main/java/com/dzeio/openhealth/data/openfoodfact/OFFProduct.kt b/app/src/main/java/com/dzeio/openhealth/data/openfoodfact/OFFProduct.kt index c6a253f..2bc9660 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/openfoodfact/OFFProduct.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/openfoodfact/OFFProduct.kt @@ -9,8 +9,20 @@ data class OFFProduct( var name: String, @SerializedName("serving_size") - var serving: String, + var servingSize: String?, + + @SerializedName("serving_quantity") + var servingQuantity: Float?, + + @SerializedName("quantity") + var quantity: String?, + + @SerializedName("product_quantity") + var productQuantity: Float?, @SerializedName("nutriments") - var nutriments: OFFNutriments + var nutriments: OFFNutriments, + + @SerializedName("image_url") + var image: String? ) diff --git a/app/src/main/java/com/dzeio/openhealth/data/openfoodfact/OpenFoodFactService.kt b/app/src/main/java/com/dzeio/openhealth/data/openfoodfact/OpenFoodFactService.kt index 3b020d2..84b7b32 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/openfoodfact/OpenFoodFactService.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/openfoodfact/OpenFoodFactService.kt @@ -1,8 +1,6 @@ package com.dzeio.openhealth.data.openfoodfact import com.google.gson.GsonBuilder -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -14,25 +12,30 @@ interface OpenFoodFactService { companion object { fun getService(): OpenFoodFactService { - val interceptor = HttpLoggingInterceptor() - interceptor.setLevel(HttpLoggingInterceptor.Level.BODY) - val client = OkHttpClient.Builder() - .addInterceptor(interceptor) - .build() +// val interceptor = HttpLoggingInterceptor() +// interceptor.setLevel(HttpLoggingInterceptor.Level.BODY) +// val client = OkHttpClient.Builder() +// .addInterceptor(interceptor) +// .build() val gson = GsonBuilder() .setLenient() .create() val retrofit = Retrofit.Builder() .baseUrl("https://world.openfoodfacts.org/") .addConverterFactory(GsonConverterFactory.create(gson)) - .client(client) +// .client(client) .build() return retrofit.create(OpenFoodFactService::class.java) } } + @Headers("User-Agent: OpenHealth - Android - Version 1.0 - https://github.com/dzeiocom/OpenHealth") + @GET("/cgi/search.pl?json=true&fields=_id,nutriments,product_name,serving_quantity,serving_size,quantity,product_quantity,image_url&action=process") + suspend fun searchProducts(@Query("search_terms2") name: String): Response + @Headers("User-Agent: OpenHealth - Android - Version 1.0 - https://github.com/dzeiocom/OpenHealth") @GET("/api/v2/search?fields=_id,nutriments,product_name,serving_quantity") - suspend fun searchProducts(@Query("product_name") name: String): Response + suspend fun findByCode(@Query("code") code: String): Response + } diff --git a/app/src/main/java/com/dzeio/openhealth/ui/food/FoodDialog.kt b/app/src/main/java/com/dzeio/openhealth/ui/food/FoodDialog.kt new file mode 100644 index 0000000..bbb54ef --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/ui/food/FoodDialog.kt @@ -0,0 +1,88 @@ +package com.dzeio.openhealth.ui.food + +import android.util.Log +import android.view.LayoutInflater +import androidx.core.widget.addTextChangedListener +import androidx.navigation.fragment.navArgs +import com.dzeio.openhealth.R +import com.dzeio.openhealth.core.BaseDialog +import com.dzeio.openhealth.databinding.DialogFoodProductBinding +import com.dzeio.openhealth.utils.DownloadImageTask +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class FoodDialog : + BaseDialog(FoodDialogViewModel::class.java) { + + private val args: FoodDialogArgs by navArgs() + + private var quantity: Float? = null + + + override val bindingInflater: (LayoutInflater) -> DialogFoodProductBinding = + DialogFoodProductBinding::inflate + + override fun onBuilderInit(builder: MaterialAlertDialogBuilder) { + super.onBuilderInit(builder) + + builder.apply { + setTitle("Product") + setIcon(R.drawable.ic_outline_fastfood_24) + setPositiveButton(R.string.validate) { dialog, _ -> + if (quantity != null) { + viewModel.saveNewQuantity(quantity!!) + } + dialog.dismiss() + } + setNegativeButton(R.string.cancel) { dialog, _ -> + if (args.deleteOnCancel) { + viewModel.delete() + } + dialog.cancel() + } + setNeutralButton(R.string.delete) { _, _ -> + viewModel.delete() + } + } + } + + + override fun onCreated() { + super.onCreated() + Log.d("FoodDialog", args.id.toString()) + viewModel.init(args.id) + + viewModel.items.observe(this) { + Log.d("FoodDialog", it.toString()) + updateGraphs(null) + + binding.serving.text = "Serving: ${it.serving}" + DownloadImageTask(binding.image).execute(it.image) + binding.quantity.setText(it.quantity.toString()) + } + + binding.quantity.addTextChangedListener { + updateGraphs(binding.quantity.text.toString().toFloatOrNull()) + } + } + + private fun updateGraphs(newQuantity: Float?) { + quantity = newQuantity + viewModel.items.value?.let { + val transformer = newQuantity ?: it.quantity + val energy = it.energy / 100 * transformer + binding.energyTxt.text = "${energy.toInt()} / 2594kcal" + binding.energyBar.progress = (100 * energy / 2594).toInt() + val proteins = it.proteins / 100 * transformer + binding.proteinsTxt.text = "${proteins.toInt()} / 130g" + binding.proteinsBar.progress = (100 * proteins / 130).toInt() + val carbohydrates = it.carbohydrates / 100 * transformer + binding.carbsTxt.text = "${carbohydrates.toInt()} / 324g" + binding.carbsBar.progress = (100 * carbohydrates / 324).toInt() + val fat = it.fat / 100 * transformer + binding.fatTxt.text = "${fat.toInt()} / 87g" + binding.fatBar.progress = (100 * fat / 87).toInt() + } + } +} diff --git a/app/src/main/java/com/dzeio/openhealth/ui/food/FoodDialogViewModel.kt b/app/src/main/java/com/dzeio/openhealth/ui/food/FoodDialogViewModel.kt new file mode 100644 index 0000000..853e458 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/ui/food/FoodDialogViewModel.kt @@ -0,0 +1,55 @@ +package com.dzeio.openhealth.ui.food + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.dzeio.openhealth.core.BaseViewModel +import com.dzeio.openhealth.data.food.Food +import com.dzeio.openhealth.data.food.FoodRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class FoodDialogViewModel @Inject internal constructor( + private val foodRepository: FoodRepository +) : BaseViewModel() { + val items: MutableLiveData = MutableLiveData() + + fun init(productId: Long) { + viewModelScope.launch { + val res = foodRepository.getById(productId) + val food = res.first() + if (food != null) { + items.postValue(food) + } + } + } + + fun delete() { + viewModelScope.launch { + val item = items.value + if (item != null) { + foodRepository.delete(item) + } + } + } + + fun saveNewQuantity(quantity: Float) { + val it = items.value + if (it == null) { + return + } + + val transformer = (quantity ?: it.quantity) / it.quantity + it.energy = it.energy * transformer + it.proteins = it.proteins * transformer + it.carbohydrates = it.carbohydrates * transformer + it.fat = it.fat * transformer + it.quantity = quantity + + viewModelScope.launch { + foodRepository.update(it) + } + } +} diff --git a/app/src/main/java/com/dzeio/openhealth/ui/food/FoodHomeFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/food/FoodHomeFragment.kt index 44f0155..a9f2b9e 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/food/FoodHomeFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/food/FoodHomeFragment.kt @@ -2,15 +2,20 @@ package com.dzeio.openhealth.ui.food import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import com.dzeio.openhealth.Application +import com.dzeio.openhealth.R import com.dzeio.openhealth.adapters.FoodAdapter import com.dzeio.openhealth.core.BaseFragment import com.dzeio.openhealth.databinding.FragmentFoodHomeBinding import com.dzeio.openhealth.ui.steps.FoodHomeViewModel import dagger.hilt.android.AndroidEntryPoint +import java.util.Calendar @AndroidEntryPoint class FoodHomeFragment : @@ -26,6 +31,9 @@ class FoodHomeFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // FIXME: deprecated + setHasOptionsMenu(true) + viewModel.init() val recycler = binding.list @@ -35,13 +43,72 @@ class FoodHomeFragment : val adapter = FoodAdapter() adapter.onItemClick = { -// findNavController().navigate( -// WaterHomeFragmentDirections.actionNavWaterHomeToNavWaterEdit( -// it.id -// ) -// ) + findNavController().navigate( + FoodHomeFragmentDirections.actionFoodHomeFragmentToNavDialogFoodProduct( + it.id, + false + ) + ) } recycler.adapter = adapter + viewModel.items.observe(viewLifecycleOwner) { + adapter.set(it) + + var energy = 0f + var proteins = 0f + var carbohydrates = 0f + var fat = 0f + for (food in it) { + energy += food.energy / 100 * food.quantity + proteins += food.proteins / 100 * food.quantity + carbohydrates += food.carbohydrates / 100 * food.quantity + fat += food.fat / 100 * food.quantity + + } + binding.energyTxt.text = "${energy.toInt()} / 2594kcal" + binding.energyBar.progress = (100 * energy / 2594).toInt() + binding.proteinsTxt.text = "${proteins.toInt()} / 130g" + binding.proteinsBar.progress = (100 * proteins / 130).toInt() + binding.carbsTxt.text = "${carbohydrates.toInt()} / 324g" + binding.carbsBar.progress = (100 * carbohydrates / 324).toInt() + binding.fatTxt.text = "${fat.toInt()} / 87g" + binding.fatBar.progress = (100 * fat / 87).toInt() + } + + binding.next.setOnClickListener { + viewModel.next() + } + + binding.previous.setOnClickListener { + viewModel.previous() + } + + viewModel.date.observe(viewLifecycleOwner) { + val date = Calendar.getInstance() + date.timeInMillis = it + binding.date.text = "${date.get(Calendar.YEAR)}-${date.get(Calendar.MONTH) + 1}-${date.get(Calendar.DAY_OF_MONTH)}" + } + } + + @Deprecated("Deprecated in Java") + override fun onPrepareOptionsMenu(menu: Menu) { + menu.findItem(R.id.action_add).isVisible = true + + super.onPrepareOptionsMenu(menu) + } + + @Deprecated("Deprecated in Java") + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_add -> { + findNavController().navigate( + FoodHomeFragmentDirections.actionFoodHomeFragmentToNavDialogFoodSearch() + ) + true + } + + else -> super.onOptionsItemSelected(item) + } } } diff --git a/app/src/main/java/com/dzeio/openhealth/ui/food/FoodHomeViewModel.kt b/app/src/main/java/com/dzeio/openhealth/ui/food/FoodHomeViewModel.kt index c0b547e..3bc5b93 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/food/FoodHomeViewModel.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/food/FoodHomeViewModel.kt @@ -5,22 +5,65 @@ import androidx.lifecycle.viewModelScope import com.dzeio.openhealth.core.BaseViewModel import com.dzeio.openhealth.data.food.Food import com.dzeio.openhealth.data.food.FoodRepository +import com.dzeio.openhealth.data.openfoodfact.OpenFoodFactService import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import java.util.Calendar import javax.inject.Inject @HiltViewModel class FoodHomeViewModel @Inject internal constructor( - private val foodRepository: FoodRepository + private val foodRepository: FoodRepository, + private val foodFactService: OpenFoodFactService ) : BaseViewModel() { val items: MutableLiveData> = MutableLiveData() + private val list: MutableLiveData> = MutableLiveData(arrayListOf()) + val date: MutableLiveData = MutableLiveData(Calendar.getInstance().timeInMillis) fun init() { + val now = Calendar.getInstance() + now.set(Calendar.HOUR, 0) + now.set(Calendar.MINUTE, 0) + now.set(Calendar.SECOND, 0) + date.postValue(now.timeInMillis) viewModelScope.launch { foodRepository.getAll().collectLatest { - items.postValue(it) + list.postValue(it) + updateList(it) } } } + + fun next() { + val now = Calendar.getInstance() + now.timeInMillis = date.value!! + now.add(Calendar.DAY_OF_YEAR, 1) + + date.value = now.timeInMillis + + updateList() + } + + fun previous() { + val now = Calendar.getInstance() + now.timeInMillis = date.value!! + now.add(Calendar.DAY_OF_YEAR, -1) + + date.value = now.timeInMillis + + updateList() + } + + private fun updateList(foods: List? = null) { + val day = Calendar.getInstance() + day.timeInMillis = date.value!! + val todayInMillis = day.timeInMillis + day.add(Calendar.DAY_OF_YEAR, 1) + val tomorrow = day.timeInMillis + + items.postValue((foods ?: list.value!!).filter { food -> + food.timestamp in todayInMillis until tomorrow + }) + } } diff --git a/app/src/main/java/com/dzeio/openhealth/ui/food/SearchFoodDialog.kt b/app/src/main/java/com/dzeio/openhealth/ui/food/SearchFoodDialog.kt new file mode 100644 index 0000000..551ec00 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/ui/food/SearchFoodDialog.kt @@ -0,0 +1,81 @@ +package com.dzeio.openhealth.ui.food + +import android.view.LayoutInflater +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.dzeio.openhealth.R +import com.dzeio.openhealth.adapters.FoodAdapter +import com.dzeio.openhealth.core.BaseDialog +import com.dzeio.openhealth.databinding.DialogFoodSearchProductBinding +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class SearchFoodDialog : + BaseDialog(SearchFoodDialogViewModel::class.java) { + + override val bindingInflater: (LayoutInflater) -> DialogFoodSearchProductBinding = + DialogFoodSearchProductBinding::inflate + + override fun onBuilderInit(builder: MaterialAlertDialogBuilder) { + super.onBuilderInit(builder) + + builder.apply { + setTitle("Add Product") + setIcon(R.drawable.ic_outline_fastfood_24) + setNegativeButton(R.string.close) { dialog, _ -> + dialog.cancel() + } + setNeutralButton("Search", null) + } + } + + override fun onDialogInit(dialog: AlertDialog) { + super.onDialogInit(dialog) + + dialog.setOnShowListener { + val btn = dialog.getButton(AlertDialog.BUTTON_NEUTRAL) + btn.setOnClickListener { + viewModel.search(binding.input.text.toString()) + binding.loading.visibility = View.VISIBLE + + } + } + } + + override fun onCreated() { + super.onCreated() + + + + val recycler = binding.list + + val manager = LinearLayoutManager(requireContext()) + recycler.layoutManager = manager + + val adapter = FoodAdapter() + adapter.onItemClick = { + + lifecycleScope.launch { + val id = viewModel.addProduct(it) + findNavController().navigate( + SearchFoodDialogDirections.actionNavDialogFoodSearchToNavDialogFoodProduct( + id, + true + ) + ) + } + } + recycler.adapter = adapter + + viewModel.items.observe(this) { + adapter.set(it) + binding.loading.visibility = View.GONE + } + + } +} diff --git a/app/src/main/java/com/dzeio/openhealth/ui/food/SearchFoodDialogViewModel.kt b/app/src/main/java/com/dzeio/openhealth/ui/food/SearchFoodDialogViewModel.kt new file mode 100644 index 0000000..0ccd279 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/ui/food/SearchFoodDialogViewModel.kt @@ -0,0 +1,37 @@ +package com.dzeio.openhealth.ui.food + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.dzeio.openhealth.core.BaseViewModel +import com.dzeio.openhealth.data.food.Food +import com.dzeio.openhealth.data.food.FoodRepository +import com.dzeio.openhealth.data.openfoodfact.OpenFoodFactService +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SearchFoodDialogViewModel @Inject internal constructor( + private val foodRepository: FoodRepository, + private val foodFactService: OpenFoodFactService +) : BaseViewModel() { + val items: MutableLiveData> = MutableLiveData() + + fun search(text: String) { + viewModelScope.launch { + val response = foodFactService.searchProducts(text) + val product = response.body() + if (product != null) { + + items.postValue(product.products + .filter { it.name != null } + .map { Food.fromOpenFoodFact(it) } + ) + } + } + } + + suspend fun addProduct(product: Food): Long { + return foodRepository.add(product) + } +} diff --git a/app/src/main/java/com/dzeio/openhealth/ui/home/HomeFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/home/HomeFragment.kt index 0e10999..ba4aeb2 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/home/HomeFragment.kt @@ -21,7 +21,6 @@ import com.dzeio.openhealth.utils.GraphUtils import com.google.android.material.color.MaterialColors import dagger.hilt.android.AndroidEntryPoint import kotlin.math.max -import kotlin.math.min @AndroidEntryPoint class HomeFragment : BaseFragment(HomeViewModel::class.java) { @@ -104,6 +103,14 @@ class HomeFragment : BaseFragment(HomeViewMo } } + viewModel.steps.observe(viewLifecycleOwner) { + binding.stepsCurrent.text = it.toString() + } + + viewModel.stepsGoal.observe(viewLifecycleOwner) { + binding.stepsTotal.text = it.toString() + } + viewModel.weights.observe(viewLifecycleOwner) { if (it != null) { updateGraph(it) @@ -169,55 +176,42 @@ class HomeFragment : BaseFragment(HomeViewMo height = binding.background.height } - val graph = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - - val canvas = Canvas(graph) - val rect = RectF( - 10f, - 15f, - 90f, - 85f - ) - -// DrawUtils.drawRect( -// canvas, -// RectF( -// 0f, -// 0f, -// 100f, -// 100f -// ), -// MaterialColors.getColor( -// requireView(), -// com.google.android.material.R.attr.colorOnPrimary -// ), -// 3f -// ) - - DrawUtils.drawArc( - canvas, - 100f, - rect, - MaterialColors.getColor( - requireView(), - com.google.android.material.R.attr.colorOnPrimary - ), - 3f - ) val animator = ValueAnimator.ofInt( - min(this.oldValue, viewModel.dailyWaterIntake.toFloat()).toInt(), - min(newValue, viewModel.dailyWaterIntake) + this.oldValue.toInt(), + newValue ) animator.duration = 300 // ms animator.addUpdateListener { - this.oldValue = 100 * it.animatedValue as Int / viewModel.dailyWaterIntake.toFloat() + this.oldValue = (it.animatedValue as Int).toFloat() + val value = 100 * it.animatedValue as Int / viewModel.dailyWaterIntake.toFloat() // Log.d("Test2", "${this.oldValue}") + val graph = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(graph) + val rect = RectF( + 10f, + 15f, + 90f, + 85f + ) + + // background Arc + DrawUtils.drawArc( + canvas, + 100f, + rect, + MaterialColors.getColor( + requireView(), + com.google.android.material.R.attr.colorOnPrimary + ), + 3f + ) + DrawUtils.drawArc( canvas, - max(this.oldValue, 1f), + max(value, 1f), rect, MaterialColors.getColor( requireView(), diff --git a/app/src/main/java/com/dzeio/openhealth/ui/home/HomeViewModel.kt b/app/src/main/java/com/dzeio/openhealth/ui/home/HomeViewModel.kt index ca919d8..30c1659 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/home/HomeViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.dzeio.openhealth.Settings import com.dzeio.openhealth.core.BaseViewModel +import com.dzeio.openhealth.data.step.StepRepository import com.dzeio.openhealth.data.water.Water import com.dzeio.openhealth.data.water.WaterRepository import com.dzeio.openhealth.data.weight.Weight @@ -22,10 +23,17 @@ import javax.inject.Inject class HomeViewModel @Inject internal constructor( private val weightRepository: WeightRepository, private val waterRepository: WaterRepository, + stepRepository: StepRepository, settings: SharedPreferences, config: Configuration ) : BaseViewModel() { + private val _steps = MutableLiveData(0) + val steps: LiveData = _steps + + private val _stepsGoal: MutableLiveData = MutableLiveData() + val stepsGoal: LiveData = _stepsGoal + private val _water = MutableLiveData(null) val water: LiveData = _water @@ -53,6 +61,14 @@ class HomeViewModel @Inject internal constructor( } } + viewModelScope.launch { + _steps.postValue(stepRepository.todaySteps()) + } + + this._stepsGoal.postValue( + config.getInt(Settings.STEPS_GOAL).value + ) + viewModelScope.launch { weightRepository.getWeights().collectLatest { _weights.postValue(it) diff --git a/app/src/main/java/com/dzeio/openhealth/utils/DownloadImageTask.kt b/app/src/main/java/com/dzeio/openhealth/utils/DownloadImageTask.kt new file mode 100644 index 0000000..19ab927 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/utils/DownloadImageTask.kt @@ -0,0 +1,30 @@ +package com.dzeio.openhealth.utils + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.AsyncTask +import android.util.Log +import android.widget.ImageView +import java.net.URL + +class DownloadImageTask(var bmImage: ImageView) : + AsyncTask() { + @Deprecated("Deprecated in Java") + override fun doInBackground(vararg urls: String?): Bitmap? { + val urldisplay = urls[0] + var mIcon11: Bitmap? = null + try { + val `in` = URL(urldisplay).openStream() + mIcon11 = BitmapFactory.decodeStream(`in`) + } catch (e: Exception) { + Log.e("Error", e.message!!) + e.printStackTrace() + } + return mIcon11 + } + + @Deprecated("Deprecated in Java", ReplaceWith("bmImage.setImageBitmap(result)")) + override fun onPostExecute(result: Bitmap?) { + bmImage.setImageBitmap(result) + } +} diff --git a/app/src/main/res/drawable/ic_baseline_chevron_left_24.xml b/app/src/main/res/drawable/ic_baseline_chevron_left_24.xml new file mode 100644 index 0000000..25a728b --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_chevron_left_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_chevron_right_24.xml b/app/src/main/res/drawable/ic_baseline_chevron_right_24.xml new file mode 100644 index 0000000..e7cf886 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_chevron_right_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/outline_fastfood_24.xml b/app/src/main/res/drawable/outline_fastfood_24.xml new file mode 100644 index 0000000..76a8c97 --- /dev/null +++ b/app/src/main/res/drawable/outline_fastfood_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/dialog_food_product.xml b/app/src/main/res/layout/dialog_food_product.xml new file mode 100644 index 0000000..d68b57c --- /dev/null +++ b/app/src/main/res/layout/dialog_food_product.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_food_search_product.xml b/app/src/main/res/layout/dialog_food_search_product.xml new file mode 100644 index 0000000..427a8cd --- /dev/null +++ b/app/src/main/res/layout/dialog_food_search_product.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_food_home.xml b/app/src/main/res/layout/fragment_food_home.xml index 00b4241..036a302 100644 --- a/app/src/main/res/layout/fragment_food_home.xml +++ b/app/src/main/res/layout/fragment_food_home.xml @@ -16,13 +16,155 @@ + android:orientation="horizontal" + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:textAlignment="center" + android:textColor="?attr/colorOnBackground" /> @@ -200,7 +200,7 @@ + android:layout_height="2dp" /> + android:textAlignment="center" + android:textColor="?attr/colorOnBackground" /> diff --git a/app/src/main/res/layout/item_food.xml b/app/src/main/res/layout/item_food.xml index 3910641..1acb548 100644 --- a/app/src/main/res/layout/item_food.xml +++ b/app/src/main/res/layout/item_food.xml @@ -1,5 +1,6 @@ + + diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 96c52cd..bed29b2 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -152,5 +152,34 @@ android:id="@+id/foodHomeFragment" tools:layout="@layout/fragment_food_home" android:name="com.dzeio.openhealth.ui.food.FoodHomeFragment" - android:label="FoodHomeFragment" /> + android:label="FoodHomeFragment" > + + + + + + + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f18e869..c69841d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -42,6 +42,8 @@ Vous avez décliné une permission, vous ne pouvez pas utiliser cette extension suaf si vous réactivez la permission manuellement Pas Poid actuel: %1$s%2$s + Supprimer + Fermer Une Erreur est survenu lors de l\'utilisation de l\'application diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0dc6b80..7eab717 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,6 +53,8 @@ Modifiy daily goal Steps Current weight: %1$s + Delete + Close