1
0
mirror of https://github.com/dzeiocom/OpenHealth.git synced 2025-04-22 19:02:16 +00:00

feat: Too much things to say.

This commit is contained in:
Florian Bouillon 2023-01-29 19:10:46 +01:00
parent 357024770a
commit 2628bc1403
Signed by: Florian Bouillon
GPG Key ID: BEEAF3722D0EBF64
36 changed files with 550 additions and 678 deletions

View File

@ -50,7 +50,6 @@ android {
storeFile = file(keystoreProperties["storeFile"] as String) storeFile = file(keystoreProperties["storeFile"] as String)
} }
} catch (_: Exception) {} } catch (_: Exception) {}
} }
} }
@ -90,14 +89,22 @@ android {
getByName("release") { getByName("release") {
// Slimmer version // Slimmer version
isMinifyEnabled = true isMinifyEnabled = false
isShrinkResources = false
isDebuggable = true isDebuggable = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
} }
getByName("debug") { getByName("debug") {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
applicationIdSuffix = ".dev" applicationIdSuffix = ".dev"
versionNameSuffix = "-dev" versionNameSuffix = "-dev"
isDebuggable = true isDebuggable = true
@ -130,7 +137,7 @@ android {
dependencies { dependencies {
// Dzeio Charts // Dzeio Charts
implementation("com.dzeio:charts:cbd5f57f8d") implementation("com.dzeio:charts:fe20f90654")
// Dzeio Crash Handler // Dzeio Crash Handler
implementation("com.dzeio:crashhandler:1.0.1") implementation("com.dzeio:crashhandler:1.0.1")

View File

@ -18,4 +18,69 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
# EnclosingMethod is required to use InnerClasses.
-keepattributes Signature, InnerClasses, EnclosingMethod
# Retrofit does reflection on method and parameter annotations.
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
# Keep annotation default values (e.g., retrofit2.http.Field.encoded).
-keepattributes AnnotationDefault
# Retain service method parameters when optimizing.
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
# Ignore annotation used for build tooling.
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
# Ignore JSR 305 annotations for embedding nullability information.
-dontwarn javax.annotation.**
# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
-dontwarn kotlin.Unit
# Top-level functions that can only be used by Kotlin.
-dontwarn retrofit2.KotlinExtensions
-dontwarn retrofit2.KotlinExtensions$*
# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface <1>
# Keep inherited services.
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface * extends <1>
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response
# With R8 full mode generic signatures are stripped for classes that are not
# kept. Suspend functions are wrapped in continuations where the type argument
# is used.
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**
# A resource is loaded with a relative path so the package of this class must be preserved.
-adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
-dontwarn org.codehaus.mojo.animal_sniffer.*
# OkHttp platform used only on JVM and when Conscrypt and other security providers are available.
-dontwarn okhttp3.internal.platform.**
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
-dontwarn org.openjsse.**
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
-dontwarn org.codehaus.mojo.animal_sniffer.*

View File

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "2acd5897bbf15393886259605a1df934", "identityHash": "414712cc283c7f1d14cde8e00da277fb",
"entities": [ "entities": [
{ {
"tableName": "Weight", "tableName": "Weight",
@ -34,10 +34,10 @@
} }
], ],
"primaryKey": { "primaryKey": {
"autoGenerate": true,
"columnNames": [ "columnNames": [
"id" "id"
], ]
"autoGenerate": true
}, },
"indices": [ "indices": [
{ {
@ -82,10 +82,10 @@
} }
], ],
"primaryKey": { "primaryKey": {
"autoGenerate": true,
"columnNames": [ "columnNames": [
"id" "id"
], ]
"autoGenerate": true
}, },
"indices": [ "indices": [
{ {
@ -130,10 +130,10 @@
} }
], ],
"primaryKey": { "primaryKey": {
"autoGenerate": true,
"columnNames": [ "columnNames": [
"id" "id"
], ]
"autoGenerate": true
}, },
"indices": [ "indices": [
{ {
@ -147,12 +147,86 @@
} }
], ],
"foreignKeys": [] "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": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
} }
], ],
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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, '2acd5897bbf15393886259605a1df934')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '414712cc283c7f1d14cde8e00da277fb')"
] ]
} }
} }

View File

@ -1,226 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "0f92ae44f4503b964d4986959a15ef4e",
"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` 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",
"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": "INTEGER",
"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": "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, '0f92ae44f4503b964d4986959a15ef4e')"
]
}
}

View File

@ -1,232 +0,0 @@
{
"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')"
]
}
}

View File

@ -7,13 +7,9 @@ import com.dzeio.openhealth.core.BaseAdapter
import com.dzeio.openhealth.core.BaseViewHolder import com.dzeio.openhealth.core.BaseViewHolder
import com.dzeio.openhealth.data.food.Food import com.dzeio.openhealth.data.food.Food
import com.dzeio.openhealth.databinding.ItemFoodBinding import com.dzeio.openhealth.databinding.ItemFoodBinding
import com.dzeio.openhealth.utils.DownloadImageTask import com.dzeio.openhealth.utils.NetworkUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.roundToInt import kotlin.math.roundToInt
class FoodAdapter : BaseAdapter<Food, ItemFoodBinding>() { class FoodAdapter : BaseAdapter<Food, ItemFoodBinding>() {
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> ItemFoodBinding override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> ItemFoodBinding
@ -26,16 +22,13 @@ class FoodAdapter : BaseAdapter<Food, ItemFoodBinding>() {
item: Food, item: Food,
position: Int position: Int
) { ) {
// Download remote picture
CoroutineScope(Dispatchers.IO).launch { if (item.image != null) {
NetworkUtils.getImageInBackground(holder.binding.productImage, item.image!!)
} }
// Download remote picture
DownloadImageTask(holder.binding.productImage).execute(item.image)
// set the food name // set the food name
holder.binding.foodName.text = item.name holder.binding.foodName.text = item.name + " ${item.id}"
// set the food description // set the food description
holder.binding.foodDescription.text = holder.itemView.context.getString( holder.binding.foodDescription.text = holder.itemView.context.getString(

View File

@ -1,6 +1,7 @@
package com.dzeio.openhealth.adapters package com.dzeio.openhealth.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.dzeio.openhealth.R import com.dzeio.openhealth.R
import com.dzeio.openhealth.core.BaseAdapter import com.dzeio.openhealth.core.BaseAdapter
@ -15,6 +16,8 @@ class StepsAdapter() : BaseAdapter<Step, LayoutItemListBinding>() {
var onItemClick: ((weight: Step) -> Unit)? = null var onItemClick: ((weight: Step) -> Unit)? = null
var isDay = false
override fun onBindData( override fun onBindData(
holder: BaseViewHolder<LayoutItemListBinding>, holder: BaseViewHolder<LayoutItemListBinding>,
item: Step, item: Step,
@ -27,7 +30,13 @@ class StepsAdapter() : BaseAdapter<Step, LayoutItemListBinding>() {
) )
// set the datetime // set the datetime
holder.binding.datetime.text = item.formatTimestamp() holder.binding.datetime.text = item.formatTimestamp(!isDay)
if (isDay) {
holder.binding.iconRight.visibility = View.GONE
} else {
holder.binding.iconRight.setImageResource(R.drawable.ic_zoom_out_map)
}
// set the callback // set the callback
holder.binding.edit.setOnClickListener { holder.binding.edit.setOnClickListener {

View File

@ -10,7 +10,6 @@ import androidx.viewbinding.ViewBinding
*/ */
abstract class BaseActivity<VB : ViewBinding>() : AppCompatActivity() { abstract class BaseActivity<VB : ViewBinding>() : AppCompatActivity() {
/** /**
* Function to inflate the Fragment Bindings * Function to inflate the Fragment Bindings
* *

View File

@ -47,5 +47,4 @@ abstract class BaseAdapter<T, VB : ViewBinding> : RecyclerView.Adapter<BaseViewH
} }
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
} }

View File

@ -8,7 +8,9 @@ import androidx.viewbinding.ViewBinding
* *
* note: Dialog crash app with viewmodel error? add @AndroidEntryPoint * note: Dialog crash app with viewmodel error? add @AndroidEntryPoint
*/ */
abstract class BaseDialog<VM : BaseViewModel, VB : ViewBinding>(private val viewModelClass: Class<VM>) : abstract class BaseDialog<VM : BaseViewModel, VB : ViewBinding>(
private val viewModelClass: Class<VM>
) :
BaseSimpleDialog<VB>() { BaseSimpleDialog<VB>() {
val viewModel by lazy { val viewModel by lazy {

View File

@ -16,7 +16,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
/** /**
* Base around the DialogFragment class to simplify usage * Base around the DialogFragment class to simplify usage
*/ */
abstract class BaseFullscreenDialog<VM : BaseViewModel, VB : ViewBinding>(private val viewModelClass: Class<VM>) : DialogFragment() { abstract class BaseFullscreenDialog<VM : BaseViewModel, VB : ViewBinding>(
private val viewModelClass: Class<VM>
) : DialogFragment() {
/** /**
* Lazyload the viewModel * Lazyload the viewModel
@ -33,7 +35,6 @@ abstract class BaseFullscreenDialog<VM : BaseViewModel, VB : ViewBinding>(privat
*/ */
abstract val bindingInflater: (LayoutInflater) -> VB abstract val bindingInflater: (LayoutInflater) -> VB
/** /**
* Function run when the dialog was created * Function run when the dialog was created
*/ */
@ -44,7 +45,6 @@ abstract class BaseFullscreenDialog<VM : BaseViewModel, VB : ViewBinding>(privat
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
_binding = bindingInflater(inflater) _binding = bindingInflater(inflater)
setHasOptionsMenu(true) setHasOptionsMenu(true)

View File

@ -39,7 +39,6 @@ abstract class BaseStaticFragment<VB : ViewBinding> : Fragment() {
return binding.root return binding.root
} }
/** /**
* Destroy binding * Destroy binding
*/ */

View File

@ -7,6 +7,5 @@ import androidx.viewbinding.ViewBinding
* Simple implementation of RecyclerView.ViewHolder to limitate usage * Simple implementation of RecyclerView.ViewHolder to limitate usage
*/ */
class BaseViewHolder<VB : ViewBinding>( class BaseViewHolder<VB : ViewBinding>(
val binding : VB val binding: VB
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root)
}

View File

@ -10,7 +10,6 @@ open class Observable<T>(baseValue: T) {
private val functionObservers: ArrayList<(T) -> Unit> = ArrayList() private val functionObservers: ArrayList<(T) -> Unit> = ArrayList()
fun addObserver(fn: (T) -> Unit) { fun addObserver(fn: (T) -> Unit) {
if (!functionObservers.contains(fn)) { if (!functionObservers.contains(fn)) {
functionObservers.add(fn) functionObservers.add(fn)
@ -38,7 +37,6 @@ open class Observable<T>(baseValue: T) {
} }
fun notifyObservers() { fun notifyObservers() {
// Notify Functions // Notify Functions
for (fn in functionObservers) { for (fn in functionObservers) {
notifyObserver(fn) notifyObserver(fn)

View File

@ -1,7 +1,6 @@
package com.dzeio.openhealth.data package com.dzeio.openhealth.data
import android.content.Context import android.content.Context
import androidx.room.AutoMigration
import androidx.room.Database import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
@ -27,12 +26,8 @@ import com.dzeio.openhealth.data.weight.WeightDao
Food::class Food::class
], ],
// TODO: go back to version 1 when the app is published // TODO: go back to version 1 when the app is published
version = 3, version = 1,
exportSchema = true, exportSchema = true
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3)
]
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {

View File

@ -57,7 +57,7 @@ data class Food(
/** /**
* When the entry was added to our Database * When the entry was added to our Database
*/ */
var timestamp: Long = Calendar.getInstance(TimeZone.getTimeZone("UTC")).timeInMillis, var timestamp: Long = Calendar.getInstance(TimeZone.getTimeZone("UTC")).timeInMillis
) { ) {
companion object { companion object {
@ -66,7 +66,10 @@ data class Food(
*/ */
fun fromOpenFoodFact(food: OFFProduct, quantity: Float? = null): Food? { fun fromOpenFoodFact(food: OFFProduct, quantity: Float? = null): Food? {
// filter out foods that we can't use in the app // filter out foods that we can't use in the app
if (food.name == null || ((food.servingSize == null || food.servingSize == "") && (food.quantity == null || food.quantity == "") && food.servingQuantity == null && food.productQuantity == null)) { if (
food.nutriments == null ||
food.name == null ||
((food.servingSize == null || food.servingSize == "") && (food.quantity == null || food.quantity == "") && food.servingQuantity == null && food.productQuantity == null)) {
return null return null
} }
@ -78,10 +81,13 @@ data class Food(
} else if (food.productQuantity != null && food.productQuantity != 0f) { } else if (food.productQuantity != null && food.productQuantity != 0f) {
eaten = food.productQuantity!! eaten = food.productQuantity!!
} else if (food.servingSize != null || food.quantity != null) { } else if (food.servingSize != null || food.quantity != null) {
Log.d("pouet", ".${food.servingSize ?: food.quantity}. .${(food.servingSize ?: food.quantity)!!.replace(Regex(" +\\w+$"), "")}. ${food}") eaten = (food.servingSize ?: food.quantity)!!.trim().replace(
eaten = (food.servingSize ?: food.quantity)!!.trim().replace(Regex(" +\\w+$"), "").toInt().toFloat() Regex(" +\\w+$"),
""
).toInt().toFloat()
} }
} }
Log.d("Food", "$food")
return Food( return Food(
name = food.name!!, name = food.name!!,
// do some slight edit on the serving to remove strange entries like `100 g` // do some slight edit on the serving to remove strange entries like `100 g`
@ -91,7 +97,8 @@ data class Food(
carbohydrates = food.nutriments.carbohydrates, carbohydrates = food.nutriments.carbohydrates,
fat = food.nutriments.fat, fat = food.nutriments.fat,
// handle case where the energy is not given in kcal but only in kj // handle case where the energy is not given in kcal but only in kj
energy = food.nutriments.energy ?: (food.nutriments.energyKJ * 0.2390057361).toFloat(), energy = food.nutriments.energy
?: (food.nutriments.energyKJ * 0.2390057361).toFloat(),
image = food.image image = food.image
) )
} }

View File

@ -1,17 +1,76 @@
package com.dzeio.openhealth.data.food package com.dzeio.openhealth.data.food
import com.dzeio.openhealth.data.openfoodfact.OFFResult
import com.dzeio.openhealth.data.openfoodfact.OpenFoodFactService import com.dzeio.openhealth.data.openfoodfact.OpenFoodFactService
import retrofit2.Response import com.dzeio.openhealth.utils.NetworkResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class FoodRepository @Inject constructor( class FoodRepository @Inject constructor(
private val dao: FoodDao, private val dao: FoodDao,
private val offSource: OpenFoodFactService private val offSource: OpenFoodFactService
) { ) {
suspend fun searchOpenFoodFact(name: String): Response<OFFResult> =
offSource.searchProducts(name) suspend fun searchFood(name: String): Flow<NetworkResult<List<Food>>> = channelFlow {
val result = NetworkResult<List<Food>>()
val items = arrayListOf<Food>()
var otherFinished = false
launch { // Search OFF
try {
val request = offSource.searchProducts(name)
if (!request.isSuccessful) {
if (otherFinished) {
result.status = NetworkResult.NetworkStatus.ERRORED
} else {
otherFinished = true
}
send(result)
return@launch
}
val offProducts =
offSource.searchProducts(name)
.body()?.products?.map { Food.fromOpenFoodFact(it) }
if (offProducts != null) {
items.addAll(offProducts.filterNotNull())
result.data = items
if (otherFinished) {
result.status = NetworkResult.NetworkStatus.FINISHED
} else {
otherFinished = true
}
send(result)
}
} catch (e: IOException) {
if (otherFinished) {
result.status = NetworkResult.NetworkStatus.ERRORED
} else {
otherFinished = true
}
send(result)
}
}
launch { // search local DB
getAll().collectLatest { list ->
val filtered = list.filter { it.name.contains(name, true) }
items.removeAll { it.id > 0 }
items.addAll(0, filtered)
result.data = items
if (otherFinished) {
result.status = NetworkResult.NetworkStatus.FINISHED
} else {
otherFinished = true
}
send(result)
}
}
}
fun getAll() = dao.getAll() fun getAll() = dao.getAll()
@ -22,5 +81,4 @@ class FoodRepository @Inject constructor(
suspend fun delete(food: Food) = dao.delete(food) suspend fun delete(food: Food) = dao.delete(food)
suspend fun update(food: Food) = dao.update(food) suspend fun update(food: Food) = dao.update(food)
} }

View File

@ -44,19 +44,24 @@ data class Step(
} }
} }
fun formatTimestamp(): String { fun formatTimestamp(removeTime: Boolean = false): String {
val formatter = DateFormat.getDateTimeInstance( val formatter = if (removeTime) {
DateFormat.SHORT, DateFormat.getDateInstance(
DateFormat.SHORT, DateFormat.SHORT,
Locale.getDefault() Locale.getDefault()
) )
} else {
DateFormat.getDateTimeInstance(
DateFormat.SHORT,
DateFormat.SHORT,
Locale.getDefault()
)
}
return formatter.format(Date(this.timestamp)) return formatter.format(Date(this.timestamp))
} }
fun isToday(): Boolean { fun isToday(): Boolean {
val it = Calendar.getInstance(TimeZone.getTimeZone("UTC")) val it = getDay()
it.timeInMillis = timestamp
it.set(Calendar.HOUR, 0)
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
@ -64,7 +69,17 @@ data class Step(
cal.set(Calendar.MINUTE, 0) cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0) cal.set(Calendar.SECOND, 0)
cal.set(Calendar.MILLISECOND, 0) cal.set(Calendar.MILLISECOND, 0)
return it.timeInMillis == cal.timeInMillis return it == cal.timeInMillis
}
fun getDay(): Long {
val it = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
timeInMillis = timestamp
set(Calendar.HOUR, 0)
set(Calendar.AM_PM, Calendar.AM)
}
return it.timeInMillis
} }
fun isCurrent(): Boolean { fun isCurrent(): Boolean {

View File

@ -7,7 +7,7 @@ import androidx.navigation.fragment.navArgs
import com.dzeio.openhealth.R import com.dzeio.openhealth.R
import com.dzeio.openhealth.core.BaseDialog import com.dzeio.openhealth.core.BaseDialog
import com.dzeio.openhealth.databinding.DialogFoodProductBinding import com.dzeio.openhealth.databinding.DialogFoodProductBinding
import com.dzeio.openhealth.utils.DownloadImageTask import com.dzeio.openhealth.utils.NetworkUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -61,7 +61,12 @@ class FoodDialog :
updateGraphs(null) updateGraphs(null)
binding.serving.text = "Serving: ${it.serving}" binding.serving.text = "Serving: ${it.serving}"
DownloadImageTask(binding.image).execute(it.image) if (it.image != null) {
NetworkUtils.getImageInBackground(
binding.image,
it.image!!
)
}
binding.quantity.setText(it.quantity.toString()) binding.quantity.setText(it.quantity.toString())
} }

View File

@ -10,13 +10,16 @@ import com.dzeio.openhealth.R
import com.dzeio.openhealth.adapters.FoodAdapter import com.dzeio.openhealth.adapters.FoodAdapter
import com.dzeio.openhealth.core.BaseDialog import com.dzeio.openhealth.core.BaseDialog
import com.dzeio.openhealth.databinding.DialogFoodSearchProductBinding import com.dzeio.openhealth.databinding.DialogFoodSearchProductBinding
import com.dzeio.openhealth.utils.NetworkResult
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@AndroidEntryPoint @AndroidEntryPoint
class SearchFoodDialog : class SearchFoodDialog :
BaseDialog<SearchFoodDialogViewModel, DialogFoodSearchProductBinding>(SearchFoodDialogViewModel::class.java) { BaseDialog<SearchFoodDialogViewModel, DialogFoodSearchProductBinding>(
SearchFoodDialogViewModel::class.java
) {
override val bindingInflater: (LayoutInflater) -> DialogFoodSearchProductBinding = override val bindingInflater: (LayoutInflater) -> DialogFoodSearchProductBinding =
DialogFoodSearchProductBinding::inflate DialogFoodSearchProductBinding::inflate
@ -42,7 +45,6 @@ class SearchFoodDialog :
btn.setOnClickListener { btn.setOnClickListener {
viewModel.search(binding.input.text.toString()) viewModel.search(binding.input.text.toString())
binding.loading.visibility = View.VISIBLE binding.loading.visibility = View.VISIBLE
} }
} }
} }
@ -50,8 +52,6 @@ class SearchFoodDialog :
override fun onCreated() { override fun onCreated() {
super.onCreated() super.onCreated()
val recycler = binding.list val recycler = binding.list
val manager = LinearLayoutManager(requireContext()) val manager = LinearLayoutManager(requireContext())
@ -59,7 +59,6 @@ class SearchFoodDialog :
val adapter = FoodAdapter() val adapter = FoodAdapter()
adapter.onItemClick = { adapter.onItemClick = {
lifecycleScope.launch { lifecycleScope.launch {
val id = viewModel.addProduct(it) val id = viewModel.addProduct(it)
findNavController().navigate( findNavController().navigate(
@ -73,9 +72,13 @@ class SearchFoodDialog :
recycler.adapter = adapter recycler.adapter = adapter
viewModel.items.observe(this) { viewModel.items.observe(this) {
adapter.set(it) adapter.set(it.data ?: arrayListOf())
binding.loading.visibility = View.GONE if (it.status == NetworkResult.NetworkStatus.FINISHED) {
binding.loading.visibility = View.GONE
} else if (it.status == NetworkResult.NetworkStatus.ERRORED) {
binding.errorText.visibility = View.VISIBLE
binding.loading.visibility = View.GONE
}
} }
} }
} }

View File

@ -5,35 +5,30 @@ import androidx.lifecycle.viewModelScope
import com.dzeio.openhealth.core.BaseViewModel import com.dzeio.openhealth.core.BaseViewModel
import com.dzeio.openhealth.data.food.Food import com.dzeio.openhealth.data.food.Food
import com.dzeio.openhealth.data.food.FoodRepository import com.dzeio.openhealth.data.food.FoodRepository
import com.dzeio.openhealth.data.openfoodfact.OpenFoodFactService import com.dzeio.openhealth.utils.NetworkResult
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.ArrayList
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SearchFoodDialogViewModel @Inject internal constructor( class SearchFoodDialogViewModel @Inject internal constructor(
private val foodRepository: FoodRepository, private val foodRepository: FoodRepository
private val foodFactService: OpenFoodFactService
) : BaseViewModel() { ) : BaseViewModel() {
val items: MutableLiveData<List<Food>> = MutableLiveData() val items: MutableLiveData<NetworkResult<List<Food>>> = MutableLiveData()
fun search(text: String) { fun search(text: String) {
viewModelScope.launch { viewModelScope.launch {
val response = foodFactService.searchProducts(text) foodRepository.searchFood(text).collectLatest {
val product = response.body() items.postValue(it)
if (product != null) {
items.postValue(
product.products
.map { Food.fromOpenFoodFact(it) }
.filter { it != null } as List<Food>
)
} }
} }
} }
suspend fun addProduct(product: Food): Long { suspend fun addProduct(product: Food): Long {
if (product.id > 0) {
product.id = 0
}
return foodRepository.add(product) return foodRepository.add(product)
} }
} }

View File

@ -4,12 +4,14 @@ import android.animation.ValueAnimator
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.dzeio.charts.Entry import com.dzeio.charts.Entry
import com.dzeio.charts.axis.Line
import com.dzeio.charts.series.LineSerie import com.dzeio.charts.series.LineSerie
import com.dzeio.openhealth.core.BaseFragment import com.dzeio.openhealth.core.BaseFragment
import com.dzeio.openhealth.data.water.Water import com.dzeio.openhealth.data.water.Water
@ -156,6 +158,13 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
} }
serie.entries = entries serie.entries = entries
if (viewModel.goalWeight.value != null) {
chart.yAxis.addLine(
viewModel.goalWeight.value!!,
Line(true, Paint(chart.yAxis.linePaint).apply { strokeWidth = 4f })
)
}
if (list.isEmpty()) { if (list.isEmpty()) {
chart.xAxis.x = 0.0 chart.xAxis.x = 0.0
} else { } else {

View File

@ -1,9 +1,12 @@
package com.dzeio.openhealth.ui.steps package com.dzeio.openhealth.ui.steps
import android.graphics.Paint
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.dzeio.charts.Entry import com.dzeio.charts.Entry
import com.dzeio.charts.axis.Line import com.dzeio.charts.axis.Line
@ -11,6 +14,7 @@ import com.dzeio.charts.series.BarSerie
import com.dzeio.openhealth.Application import com.dzeio.openhealth.Application
import com.dzeio.openhealth.adapters.StepsAdapter import com.dzeio.openhealth.adapters.StepsAdapter
import com.dzeio.openhealth.core.BaseFragment import com.dzeio.openhealth.core.BaseFragment
import com.dzeio.openhealth.data.step.Step
import com.dzeio.openhealth.databinding.FragmentStepsHomeBinding import com.dzeio.openhealth.databinding.FragmentStepsHomeBinding
import com.dzeio.openhealth.utils.ChartUtils import com.dzeio.openhealth.utils.ChartUtils
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -29,6 +33,8 @@ class StepsHomeFragment :
const val TAG = "${Application.TAG}/SHFragment" const val TAG = "${Application.TAG}/SHFragment"
} }
private val args: StepsHomeFragmentArgs by navArgs()
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentStepsHomeBinding = override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentStepsHomeBinding =
FragmentStepsHomeBinding::inflate FragmentStepsHomeBinding::inflate
@ -37,18 +43,26 @@ class StepsHomeFragment :
viewModel.init() viewModel.init()
val isDay = args.day > 0L
val recycler = binding.list val recycler = binding.list
val manager = LinearLayoutManager(requireContext()) val manager = LinearLayoutManager(requireContext())
recycler.layoutManager = manager recycler.layoutManager = manager
val adapter = StepsAdapter() val adapter = StepsAdapter().apply {
adapter.onItemClick = { this.isDay = isDay
// findNavController().navigate( }
// WaterHomeFragmentDirections.actionNavWaterHomeToNavWaterEdit(
// it.id if (!isDay) {
// ) adapter.onItemClick = {
// ) findNavController().navigate(
StepsHomeFragmentDirections.actionNavStepsHomeSelf().apply {
day = it.timestamp
title = "Steps from " + it.formatTimestamp(true)
}
)
}
} }
recycler.adapter = adapter recycler.adapter = adapter
@ -64,69 +78,103 @@ class StepsHomeFragment :
} }
xAxis.apply { xAxis.apply {
dataWidth = 604800000.0 dataWidth = if (isDay) 8.64e+7 else 6.048e+8
scrollEnabled = !isDay
textPaint.textSize = 32f textPaint.textSize = 32f
onValueFormat = onValueFormat@{ onValueFormat = onValueFormat@{
val formatter = DateFormat.getDateInstance( val formatter = if (isDay) {
DateFormat.SHORT, DateFormat.getTimeInstance(
Locale.getDefault() DateFormat.SHORT,
) Locale.getDefault()
)
} else {
DateFormat.getDateInstance(
DateFormat.SHORT,
Locale.getDefault()
)
}
return@onValueFormat formatter.format(Date(it.toLong())) return@onValueFormat formatter.format(Date(it.toLong()))
} }
} }
annotator.annotationTitleFormat = { "${it.y.roundToInt()} steps" } annotator.annotationTitleFormat = { "${it.y.roundToInt()} steps" }
annotator.annotationSubTitleFormat = annotationSubTitleFormat@{ annotator.annotationSubTitleFormat = annotationSubTitleFormat@{
val formatter = DateFormat.getDateInstance( val formatter = if (isDay) {
DateFormat.SHORT, DateFormat.getTimeInstance(
Locale.getDefault() DateFormat.SHORT,
) Locale.getDefault()
)
} else {
DateFormat.getDateInstance(
DateFormat.SHORT,
Locale.getDefault()
)
}
return@annotationSubTitleFormat formatter.format(Date(it.x.toLong())) return@annotationSubTitleFormat formatter.format(Date(it.x.toLong()))
} }
} }
viewModel.goal.observe(viewLifecycleOwner) { viewModel.goal.observe(viewLifecycleOwner) {
if (it != null) { if (it != null && !isDay) {
chart.yAxis.addLine(it.toFloat(), Line(true)) chart.yAxis.addLine(
it.toFloat(),
Line(true, Paint(chart.yAxis.linePaint).apply { strokeWidth = 4f })
)
chart.refresh() chart.refresh()
} }
} }
viewModel.items.observe(viewLifecycleOwner) { list -> viewModel.items.observe(viewLifecycleOwner) { list ->
adapter.set(list)
if (list.isEmpty()) { if (list.isEmpty()) {
adapter.set(arrayListOf())
return@observe return@observe
} }
// chart.scroller.zoomEnabled = false val filtered = if (!isDay) list else list.filter {
it.getDay() == args.day
}
if (isDay) {
adapter.set(filtered)
serie.entries = filtered.map {
Entry(
it.timestamp.toDouble(),
it.value.toFloat()
)
} as ArrayList<Entry>
} else {
val entries: HashMap<Long, Entry> = HashMap()
val entries: HashMap<Long, Entry> = HashMap() list.forEach {
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
cal.timeInMillis = it.timestamp
list.forEach { cal.set(Calendar.HOUR, 0)
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) cal.set(Calendar.AM_PM, Calendar.AM)
cal.timeInMillis = it.timestamp val ts = cal.timeInMillis
if (!entries.containsKey(ts)) {
entries[ts] = Entry((ts).toDouble(), 0F, chart.yAxis.goalLinePaint.color)
}
cal.set(Calendar.HOUR, 0) entries[ts]!!.y += it.value.toFloat()
cal.set(Calendar.AM_PM, Calendar.AM)
val ts = cal.timeInMillis
if (!entries.containsKey(ts)) {
entries[ts] = Entry((ts).toDouble(), 0F, chart.yAxis.goalLinePaint.color)
}
entries[ts]!!.y += it.value.toFloat() if (viewModel.goal.value != null) {
if (entries[ts]!!.y > viewModel.goal.value!!) {
if (viewModel.goal.value != null) { entries[ts]!!.color = null
if (entries[ts]!!.y > viewModel.goal.value!!) { }
} else {
entries[ts]!!.color = null entries[ts]!!.color = null
} }
} else {
entries[ts]!!.color = null
} }
adapter.set(
entries.map { Step(value = it.value.y.toInt(), timestamp = it.key) }
.sortedByDescending { it.timestamp }
)
serie.entries = ArrayList(entries.values)
} }
serie.entries = ArrayList(entries.values) chart.xAxis.x =
chart.xAxis.getXMax() - chart.xAxis.dataWidth!! + chart.xAxis.dataWidth!! / (if (isDay) 24 else 7)
chart.xAxis.x = chart.xAxis.getXMin()
chart.refresh() chart.refresh()
} }

View File

@ -1,16 +1,20 @@
package com.dzeio.openhealth.ui.weight package com.dzeio.openhealth.ui.weight
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Paint
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.MenuProvider
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.dzeio.charts.Entry import com.dzeio.charts.Entry
import com.dzeio.charts.axis.Line
import com.dzeio.charts.series.LineSerie import com.dzeio.charts.series.LineSerie
import com.dzeio.openhealth.R import com.dzeio.openhealth.R
import com.dzeio.openhealth.adapters.WeightAdapter import com.dzeio.openhealth.adapters.WeightAdapter
@ -37,8 +41,27 @@ class ListWeightFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// FIXME: deprecated // Menu
setHasOptionsMenu(true) requireActivity().addMenuProvider(object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menu.findItem(R.id.action_add).isVisible = true
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_add -> {
findNavController().navigate(
ListWeightFragmentDirections.actionNavListWeightToNavWeightDialog(
WeightDialog.DialogTypes.ADD_WEIGHT.ordinal
)
)
true
}
else -> true
}
}
})
if (viewModel.goalWeight.value != null) { if (viewModel.goalWeight.value != null) {
binding.goalButton.setText(R.string.edit_goal) binding.goalButton.setText(R.string.edit_goal)
@ -46,7 +69,9 @@ class ListWeightFragment :
binding.goalButton.setOnClickListener { binding.goalButton.setOnClickListener {
findNavController().navigate( findNavController().navigate(
ListWeightFragmentDirections.actionNavListWeightToNavWeightDialog(WeightDialog.DialogTypes.EDIT_GOAL.ordinal) ListWeightFragmentDirections.actionNavListWeightToNavWeightDialog(
WeightDialog.DialogTypes.EDIT_GOAL.ordinal
)
) )
} }
@ -110,7 +135,6 @@ class ListWeightFragment :
xAxis.apply { xAxis.apply {
// 7 day history // 7 day history
dataWidth = (7 * 24 * 60 * 60 * 1000).toDouble()
textPaint.color = MaterialColors.getColor( textPaint.color = MaterialColors.getColor(
requireView(), requireView(),
com.google.android.material.R.attr.colorOnPrimaryContainer com.google.android.material.R.attr.colorOnPrimaryContainer
@ -128,15 +152,15 @@ class ListWeightFragment :
} }
// Debug button // Debug button
if (binding.debugRandomValues != null) { // if (binding.debugRandomValues != null) {
binding.debugRandomValues.setOnClickListener { // binding.debugRandomValues.setOnClickListener {
viewModel.generateRandomValues() // viewModel.generateRandomValues()
} // }
binding.debugRandomValues.setOnLongClickListener { // binding.debugRandomValues.setOnLongClickListener {
viewModel.delete(viewModel.weights.value!!) // viewModel.delete(viewModel.weights.value!!)
return@setOnLongClickListener true // return@setOnLongClickListener true
} // }
} // }
} }
private fun updateGraph(list: List<Weight>) { private fun updateGraph(list: List<Weight>) {
@ -146,13 +170,22 @@ class ListWeightFragment :
val entries: ArrayList<Entry> = arrayListOf() val entries: ArrayList<Entry> = arrayListOf()
list.forEach { list.forEach {
entries.add(Entry( entries.add(
it.timestamp.toDouble(), Entry(
it.weight it.timestamp.toDouble(),
)) it.weight
)
)
} }
serie.entries = entries serie.entries = entries
if (viewModel.goalWeight.value != null) {
chart.yAxis.addLine(
viewModel.goalWeight.value!!,
Line(true, Paint(chart.yAxis.linePaint).apply { strokeWidth = 4f })
)
}
if (list.isEmpty()) { if (list.isEmpty()) {
chart.xAxis.x = 0.0 chart.xAxis.x = 0.0
} else { } else {
@ -161,27 +194,4 @@ class ListWeightFragment :
chart.refresh() chart.refresh()
} }
@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(
ListWeightFragmentDirections.actionNavListWeightToNavWeightDialog(
WeightDialog.DialogTypes.ADD_WEIGHT.ordinal
)
)
true
}
else -> super.onOptionsItemSelected(item)
}
}
} }

View File

@ -21,7 +21,6 @@ class ListWeightViewModel @Inject internal constructor(
private val settings: Configuration private val settings: Configuration
) : BaseViewModel() { ) : BaseViewModel() {
private val _massUnit = MutableLiveData(Units.Mass.KILOGRAM) private val _massUnit = MutableLiveData(Units.Mass.KILOGRAM)
val massUnit: LiveData<Units.Mass> = _massUnit val massUnit: LiveData<Units.Mass> = _massUnit
@ -31,7 +30,6 @@ class ListWeightViewModel @Inject internal constructor(
private val _weights = MutableLiveData<List<Weight>?>(null) private val _weights = MutableLiveData<List<Weight>?>(null)
val weights: LiveData<List<Weight>?> = _weights val weights: LiveData<List<Weight>?> = _weights
init { init {
viewModelScope.launch { viewModelScope.launch {
weightRepository.getWeights().collectLatest { weightRepository.getWeights().collectLatest {
@ -50,7 +48,7 @@ class ListWeightViewModel @Inject internal constructor(
} }
} }
fun generateRandomValues(): Unit { fun generateRandomValues() {
viewModelScope.launch { viewModelScope.launch {
weightRepository.addWeight( weightRepository.addWeight(
Weight( Weight(

View File

@ -1,37 +0,0 @@
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
/**
* Stolen from StackOverflow https://stackoverflow.com/a/10868126/7335674
*
* Allows to download an image asynchronously
*
* TODO: rework so it is not deprecated anymore
*/
class DownloadImageTask(var bmImage: ImageView) :
AsyncTask<String?, Void?, Bitmap?>() {
@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)
}
}

View File

@ -0,0 +1,12 @@
package com.dzeio.openhealth.utils
data class NetworkResult<T>(
var status: NetworkStatus = NetworkStatus.RUNNING,
var data: T? = null
) {
enum class NetworkStatus {
RUNNING,
FINISHED,
ERRORED
}
}

View File

@ -0,0 +1,39 @@
package com.dzeio.openhealth.utils
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.ImageView
import java.net.URL
import java.util.concurrent.Executors
object NetworkUtils {
/**
* Fetch an image and apply it to an [image] [ImageView]
*
* Adapted from https://stackoverflow.com/a/10868126/7335674
* to not be deprecated
*
* @param image the ImageView
* @param url the url to fetch the image from
*/
fun getImageInBackground(image: ImageView, url: String) {
val executor = Executors.newSingleThreadExecutor()
val handler = Handler(Looper.getMainLooper())
executor.execute {
var bitmap: Bitmap? = null
try {
val `in` = URL(url).openStream()
bitmap = BitmapFactory.decodeStream(`in`)
handler.post {
image.setImageBitmap(bitmap)
}
} catch (e: Exception) {
Log.e("Error", e.message!!)
e.printStackTrace()
}
}
}
}

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="?attr/colorControlNormal" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M15,3l2.3,2.3l-2.89,2.87l1.42,1.42L18.7,6.7L21,9V3H15zM3,9l2.3,-2.3l2.87,2.89l1.42,-1.42L6.7,5.3L9,3H3V9zM9,21l-2.3,-2.3l2.89,-2.87l-1.42,-1.42L5.3,17.3L3,15v6H9zM21,15l-2.3,2.3l-2.87,-2.89l-1.42,1.42l2.89,2.87L15,21h6V15z"/>
</vector>

View File

@ -19,12 +19,21 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<TextView
android:id="@+id/error_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:text="@string/connectivity_error" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/list" android:id="@+id/list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:itemCount="5" tools:itemCount="5"
tools:listitem="@layout/item_food"/> tools:listitem="@layout/item_food">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout> </LinearLayout>

View File

@ -53,7 +53,7 @@
</LinearLayout> </LinearLayout>
<com.github.mikephil.charting.charts.LineChart <com.dzeio.charts.ChartView
android:id="@+id/chart" android:id="@+id/chart"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="200dp" android:layout_height="200dp"

View File

@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="?attr/materialCardViewFilledStyle" style="?attr/materialCardViewFilledStyle"
xmlns:tools="http://schemas.android.com/tools"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
@ -41,10 +39,11 @@
</LinearLayout> </LinearLayout>
<ImageView <ImageView
android:id="@+id/icon_right"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:src="@drawable/ic_baseline_edit_24" /> android:src="@drawable/ic_baseline_edit_24" />
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>

View File

@ -134,9 +134,23 @@
tools:layout="@layout/fragment_activity" /> tools:layout="@layout/fragment_activity" />
<fragment <fragment
android:id="@+id/nav_steps_home" android:id="@+id/nav_steps_home"
android:label="{title}"
android:name="com.dzeio.openhealth.ui.steps.StepsHomeFragment" android:name="com.dzeio.openhealth.ui.steps.StepsHomeFragment"
android:label="@string/menu_steps" tools:layout="@layout/fragment_steps_home" >
tools:layout="@layout/fragment_steps_home" /> <argument
android:name="day"
app:argType="long"
android:defaultValue="-1L"
/>
<argument
android:name="title"
app:argType="string"
android:defaultValue="Steps"
/>
<action
android:id="@+id/action_nav_steps_home_self"
app:destination="@id/nav_steps_home" />
</fragment>
<dialog <dialog
android:id="@+id/nav_weight_dialog" android:id="@+id/nav_weight_dialog"

View File

@ -54,5 +54,6 @@
<string name="steps_taken">Pas pris</string> <string name="steps_taken">Pas pris</string>
<string name="error_reporter_crash">Erreur lors de la géneration d\'un rapport d\'erreur</string> <string name="error_reporter_crash">Erreur lors de la géneration d\'un rapport d\'erreur</string>
<string name="steps_count">%1$d pas</string> <string name="steps_count">%1$d pas</string>
<string name="connectivity_error">Il semplerais que nous ne pouvons pas communiquer avec OpenFoodFact, Merci de re-essayer plus tard</string>
</resources> </resources>

View File

@ -67,4 +67,5 @@
<string name="error_reporter_crash">An error occurred while making the error report</string> <string name="error_reporter_crash">An error occurred while making the error report</string>
<string name="food_description" translatable="false">%1$s (%2$.0f kcal)</string> <string name="food_description" translatable="false">%1$s (%2$.0f kcal)</string>
<string name="steps_count">%1$d steps</string> <string name="steps_count">%1$d steps</string>
<string name="connectivity_error">It seems that we can\'t communicate with OpenFoodFact, please retry later</string>
</resources> </resources>

View File

@ -14,9 +14,9 @@ buildscript {
plugins { plugins {
// android app plugin ? (tbh idk what thoses "plugins" does) // android app plugin ? (tbh idk what thoses "plugins" does)
id("com.android.application") version "7.3.1" apply false id("com.android.application") version "7.4.0" apply false
// is it a lib? no, do I need it? IDK // is it a lib? no, do I need it? IDK
id("com.android.library") version "7.3.1" apply false id("com.android.library") version "7.4.0" apply false
// add kotlin compatibility :> // add kotlin compatibility :>
id("org.jetbrains.kotlin.android") version "1.8.0" apply false id("org.jetbrains.kotlin.android") version "1.8.0" apply false