Merge pull request #112 from kirmanak/ingredient-amounts

Add support for ingredient amounts
This commit is contained in:
Kirill Kamakin
2022-12-04 19:17:39 +01:00
committed by GitHub
18 changed files with 471 additions and 37 deletions

View File

@@ -15,8 +15,8 @@ plugins {
android {
defaultConfig {
applicationId = "gq.kirmanak.mealient"
versionCode = 23
versionName = "0.3.8"
versionCode = 24
versionName = "0.3.9"
}
signingConfigs {

View File

@@ -6,10 +6,18 @@ data class FullRecipeInfo(
val recipeYield: String,
val recipeIngredients: List<RecipeIngredientInfo>,
val recipeInstructions: List<RecipeInstructionInfo>,
val settings: RecipeSettingsInfo,
)
data class RecipeSettingsInfo(
val disableAmounts: Boolean,
)
data class RecipeIngredientInfo(
val note: String,
val quantity: Double?,
val unit: String?,
val food: String?,
)
data class RecipeInstructionInfo(

View File

@@ -8,6 +8,7 @@ import gq.kirmanak.mealient.data.baseurl.VersionInfo
import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeInstructionInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeSettingsInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo
import gq.kirmanak.mealient.data.share.ParseRecipeURLInfo
import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity
@@ -31,6 +32,7 @@ import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSettingsResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
@@ -40,12 +42,16 @@ import java.util.*
fun FullRecipeInfo.toRecipeEntity() = RecipeEntity(
remoteId = remoteId,
recipeYield = recipeYield
recipeYield = recipeYield,
disableAmounts = settings.disableAmounts,
)
fun RecipeIngredientInfo.toRecipeIngredientEntity(remoteId: String) = RecipeIngredientEntity(
recipeId = remoteId,
note = note,
unit = unit,
food = food,
quantity = quantity,
)
fun RecipeInstructionInfo.toRecipeInstructionEntity(remoteId: String) = RecipeInstructionEntity(
@@ -114,11 +120,15 @@ fun GetRecipeResponseV0.toFullRecipeInfo() = FullRecipeInfo(
name = name,
recipeYield = recipeYield,
recipeIngredients = recipeIngredients.map { it.toRecipeIngredientInfo() },
recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() }
recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() },
settings = RecipeSettingsInfo(disableAmounts = true)
)
fun GetRecipeIngredientResponseV0.toRecipeIngredientInfo() = RecipeIngredientInfo(
note = note,
unit = null,
food = null,
quantity = 1.0,
)
fun GetRecipeInstructionResponseV0.toRecipeInstructionInfo() = RecipeInstructionInfo(
@@ -130,11 +140,19 @@ fun GetRecipeResponseV1.toFullRecipeInfo() = FullRecipeInfo(
name = name,
recipeYield = recipeYield,
recipeIngredients = recipeIngredients.map { it.toRecipeIngredientInfo() },
recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() }
recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() },
settings = settings.toRecipeSettingsInfo(),
)
private fun GetRecipeSettingsResponseV1.toRecipeSettingsInfo() = RecipeSettingsInfo(
disableAmounts = disableAmount,
)
fun GetRecipeIngredientResponseV1.toRecipeIngredientInfo() = RecipeIngredientInfo(
note = note,
unit = unit?.name,
food = food?.name,
quantity = quantity,
)
fun GetRecipeInstructionResponseV1.toRecipeInstructionInfo() = RecipeInstructionInfo(

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.content.res.Resources
import android.os.Build
import android.view.View
import android.view.inputmethod.InputMethodManager
@@ -21,6 +22,7 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.viewbinding.ViewBinding
import com.google.android.material.textfield.TextInputLayout
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.channels.ChannelResult
@@ -133,3 +135,6 @@ fun <T> LifecycleOwner.collectWhenResumed(flow: Flow<T>, collector: FlowCollecto
}
}
}
val <T : ViewBinding> T.resources: Resources
get() = root.resources

View File

@@ -4,6 +4,7 @@ import androidx.recyclerview.widget.RecyclerView
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
import gq.kirmanak.mealient.extensions.resources
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
import javax.inject.Inject
@@ -30,7 +31,7 @@ class RecipeViewHolder private constructor(
}
private val loadingPlaceholder by lazy {
binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder)
binding.resources.getString(R.string.view_holder_recipe_text_placeholder)
}
fun bind(item: RecipeSummaryEntity?) {

View File

@@ -19,7 +19,7 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
private val binding by viewBinding(FragmentRecipeInfoBinding::bind)
private val viewModel by viewModels<RecipeInfoViewModel>()
private val ingredientsAdapter by lazy { recipeIngredientsAdapterFactory.build() }
private lateinit var ingredientsAdapter: RecipeIngredientsAdapter
private val instructionsAdapter by lazy { recipeInstructionsAdapterFactory.build() }
@Inject
@@ -48,7 +48,6 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
logger.v { "onViewCreated() called" }
with(binding) {
ingredientsList.adapter = ingredientsAdapter
instructionsList.adapter = instructionsAdapter
}
@@ -59,6 +58,10 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
private fun onUiStateChange(uiState: RecipeInfoUiState) = with(binding) {
logger.v { "onUiStateChange() called" }
if (::ingredientsAdapter.isInitialized.not()) {
ingredientsAdapter = recipeIngredientsAdapterFactory.build(uiState.disableAmounts)
ingredientsList.adapter = ingredientsAdapter
}
ingredientsHolder.isVisible = uiState.showIngredients
instructionsGroup.isVisible = uiState.showInstructions
recipeImageLoader.loadRecipeImage(image, uiState.summaryEntity)

View File

@@ -12,4 +12,5 @@ data class RecipeInfoUiState(
val recipeInstructions: List<RecipeInstructionEntity> = emptyList(),
val title: String? = null,
val description: String? = null,
val disableAmounts: Boolean = true,
)

View File

@@ -25,10 +25,11 @@ class RecipeInfoViewModel @Inject constructor(
showIngredients = entity.recipeIngredients.isNotEmpty(),
showInstructions = entity.recipeInstructions.isNotEmpty(),
summaryEntity = entity.recipeSummaryEntity,
recipeIngredients = entity.recipeIngredients.filter { it.note.isNotBlank() },
recipeInstructions = entity.recipeInstructions.filter { it.text.isNotBlank() },
recipeIngredients = entity.recipeIngredients,
recipeInstructions = entity.recipeInstructions,
title = entity.recipeSummaryEntity.name,
description = entity.recipeSummaryEntity.description,
disableAmounts = entity.recipeEntity.disableAmounts,
)
} ?: RecipeInfoUiState()
emit(state)

View File

@@ -2,6 +2,7 @@ package gq.kirmanak.mealient.ui.recipes.info
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
@@ -15,6 +16,7 @@ import javax.inject.Singleton
class RecipeIngredientsAdapter private constructor(
private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory,
private val logger: Logger,
private val disableAmounts: Boolean,
) : ListAdapter<RecipeIngredientEntity, RecipeIngredientViewHolder>(RecipeIngredientDiffCallback) {
@Singleton
@@ -22,12 +24,17 @@ class RecipeIngredientsAdapter private constructor(
private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory,
private val logger: Logger,
) {
fun build() = RecipeIngredientsAdapter(recipeIngredientViewHolderFactory, logger)
fun build(disableAmounts: Boolean) = RecipeIngredientsAdapter(
recipeIngredientViewHolderFactory = recipeIngredientViewHolderFactory,
logger = logger,
disableAmounts = disableAmounts,
)
}
class RecipeIngredientViewHolder private constructor(
private val binding: ViewHolderIngredientBinding,
private val logger: Logger,
private val disableAmounts: Boolean,
) : RecyclerView.ViewHolder(binding.root) {
@Singleton
@@ -35,25 +42,38 @@ class RecipeIngredientsAdapter private constructor(
private val logger: Logger,
) {
fun build(binding: ViewHolderIngredientBinding) =
RecipeIngredientViewHolder(binding, logger)
fun build(
binding: ViewHolderIngredientBinding,
disableAmounts: Boolean,
) = RecipeIngredientViewHolder(
binding = binding,
logger = logger,
disableAmounts = disableAmounts,
)
}
fun bind(item: RecipeIngredientEntity) {
logger.v { "bind() called with: item = $item" }
binding.checkBox.text = item.note
binding.checkBox.text = if (disableAmounts) {
item.note
} else {
val builder = StringBuilder()
item.quantity?.let { builder.append("${it.formatUsingMediantMethod()} ") }
item.unit?.let { builder.append("$it ") }
item.food?.let { builder.append("$it ") }
builder.append(item.note)
builder.toString().trim()
}
}
}
private object RecipeIngredientDiffCallback : DiffUtil.ItemCallback<RecipeIngredientEntity>() {
override fun areItemsTheSame(
oldItem: RecipeIngredientEntity,
newItem: RecipeIngredientEntity
oldItem: RecipeIngredientEntity, newItem: RecipeIngredientEntity
): Boolean = oldItem.localId == newItem.localId
override fun areContentsTheSame(
oldItem: RecipeIngredientEntity,
newItem: RecipeIngredientEntity
oldItem: RecipeIngredientEntity, newItem: RecipeIngredientEntity
): Boolean = oldItem == newItem
}
@@ -61,7 +81,8 @@ class RecipeIngredientsAdapter private constructor(
logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" }
val inflater = LayoutInflater.from(parent.context)
return recipeIngredientViewHolderFactory.build(
ViewHolderIngredientBinding.inflate(inflater, parent, false)
ViewHolderIngredientBinding.inflate(inflater, parent, false),
disableAmounts,
)
}
@@ -72,3 +93,64 @@ class RecipeIngredientsAdapter private constructor(
holder.bind(item)
}
}
private fun Double.formatUsingMediantMethod(d: Int = 10, mixed: Boolean = true): String {
val triple = mediantMethod(d, mixed)
return when {
triple.second == 0 -> "${triple.first}"
triple.first == 0 -> "${triple.second}/${triple.third}"
else -> "${triple.first} ${triple.second}/${triple.third}"
}
}
/**
* Rational approximation to a floating point number with bounded denominator using Mediant Method.
* For example, 333/1000 will become [0, 1, 3] (1/3), 1414/1000 will be [1, 2, 5] (1 2/5).
* Uses algorithm from this npm package - https://www.npmjs.com/package/frac
* Can be seen here https://github.com/SheetJS/frac/blob/d07f3c99c7dc059fb47d391bcb3da80f4956608e/frac.js
* @receiver - number that needs to be approximated
* @param d - maximum denominator (i.e. if 10 then 17/20 will be 4/5, if 20 then 17/20).
* @param mixed - if true returns a mixed fraction otherwise improper (i.e. "7/5" or "1 2/5")
*/
@VisibleForTesting
fun Double.mediantMethod(d: Int = 10, mixed: Boolean = true): Triple<Int, Int, Int> {
val x = this
var n1 = x.toInt()
var d1 = 1
var n2 = n1 + 1
var d2 = 1
if (x != n1.toDouble()) {
while (d1 <= d && d2 <= d) {
val m = (n1 + n2).toDouble() / (d1 + d2)
when {
x == m -> {
when {
d1 + d2 <= d -> {
d1 += d2
n1 += n2
d2 = d + 1
}
d1 > d2 -> d2 = d + 1
else -> d1 = d + 1
}
break
}
x < m -> {
n2 += n1
d2 += d1
}
else -> {
n1 += n2
d1 += d2
}
}
}
}
if (d1 > d) {
d1 = d2
n1 = n2
}
if (!mixed) return Triple(0, n1, d1)
val q = (n1.toDouble() / d1).toInt()
return Triple(q, n1 - q * d1, d1)
}

View File

@@ -8,6 +8,7 @@ import androidx.recyclerview.widget.RecyclerView
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding
import gq.kirmanak.mealient.extensions.resources
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.recipes.info.RecipeInstructionsAdapter.RecipeInstructionViewHolder
import javax.inject.Inject
@@ -52,7 +53,7 @@ class RecipeInstructionsAdapter private constructor(
fun bind(item: RecipeInstructionEntity, position: Int) {
logger.v { "bind() called with: item = $item, position = $position" }
binding.step.text = binding.root.resources.getString(
binding.step.text = binding.resources.getString(
R.string.view_holder_recipe_instructions_step, position + 1
)
binding.instruction.text = item.text

View File

@@ -8,10 +8,33 @@ import gq.kirmanak.mealient.data.baseurl.VersionInfo
import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeInstructionInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeSettingsInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo
import gq.kirmanak.mealient.database.recipe.entity.*
import gq.kirmanak.mealient.datasource.v0.models.*
import gq.kirmanak.mealient.datasource.v1.models.*
import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeIngredientV0
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeInstructionV0
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeSettingsV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeIngredientResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeInstructionResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeIngredientV1
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeInstructionV1
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeSettingsV1
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSettingsResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
@@ -74,14 +97,23 @@ object RecipeImplTestData {
val SUGAR_INGREDIENT = RecipeIngredientInfo(
note = "2 oz of white sugar",
quantity = 1.0,
unit = null,
food = null,
)
val BREAD_INGREDIENT = RecipeIngredientInfo(
note = "2 oz of white bread",
quantity = 1.0,
unit = null,
food = null,
)
private val MILK_INGREDIENT = RecipeIngredientInfo(
note = "2 oz of white milk",
quantity = 1.0,
unit = null,
food = null,
)
val MIX_INSTRUCTION = RecipeInstructionInfo(
@@ -101,7 +133,8 @@ object RecipeImplTestData {
name = "Cake",
recipeYield = "4 servings",
recipeIngredients = listOf(SUGAR_INGREDIENT, BREAD_INGREDIENT),
recipeInstructions = listOf(MIX_INSTRUCTION, BAKE_INSTRUCTION)
recipeInstructions = listOf(MIX_INSTRUCTION, BAKE_INSTRUCTION),
settings = RecipeSettingsInfo(disableAmounts = true)
)
val PORRIDGE_FULL_RECIPE_INFO = FullRecipeInfo(
@@ -109,7 +142,8 @@ object RecipeImplTestData {
name = "Porridge",
recipeYield = "3 servings",
recipeIngredients = listOf(SUGAR_INGREDIENT, MILK_INGREDIENT),
recipeInstructions = listOf(MIX_INSTRUCTION, BOIL_INSTRUCTION)
recipeInstructions = listOf(MIX_INSTRUCTION, BOIL_INSTRUCTION),
settings = RecipeSettingsInfo(disableAmounts = true)
)
val MIX_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
@@ -124,17 +158,24 @@ object RecipeImplTestData {
val CAKE_RECIPE_ENTITY = RecipeEntity(
remoteId = "1",
recipeYield = "4 servings"
recipeYield = "4 servings",
disableAmounts = true,
)
val CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "1",
note = "2 oz of white sugar",
quantity = 1.0,
unit = null,
food = null,
)
val CAKE_BREAD_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "1",
note = "2 oz of white bread",
quantity = 1.0,
unit = null,
food = null,
)
val FULL_CAKE_INFO_ENTITY = FullRecipeEntity(
@@ -152,17 +193,24 @@ object RecipeImplTestData {
private val PORRIDGE_RECIPE_ENTITY_FULL = RecipeEntity(
remoteId = "2",
recipeYield = "3 servings"
recipeYield = "3 servings",
disableAmounts = true,
)
private val PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "2",
note = "2 oz of white milk",
quantity = 1.0,
unit = null,
food = null,
)
private val PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "2",
note = "2 oz of white sugar",
quantity = 1.0,
unit = null,
food = null,
)
private val PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
@@ -253,11 +301,26 @@ object RecipeImplTestData {
val SUGAR_RECIPE_INGREDIENT_RESPONSE_V0 = GetRecipeIngredientResponseV0("2 oz of white sugar")
val MILK_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1("2 oz of white milk")
val MILK_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1(
note = "2 oz of white milk",
quantity = 1.0,
unit = null,
food = null,
)
val SUGAR_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1("2 oz of white sugar")
val SUGAR_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1(
note = "2 oz of white sugar",
quantity = 1.0,
unit = null,
food = null,
)
val MILK_RECIPE_INGREDIENT_INFO = RecipeIngredientInfo("2 oz of white milk")
val MILK_RECIPE_INGREDIENT_INFO = RecipeIngredientInfo(
note = "2 oz of white milk",
quantity = 1.0,
unit = null,
food = null,
)
val MIX_RECIPE_INSTRUCTION_RESPONSE_V0 = GetRecipeInstructionResponseV0("Mix the ingredients")
@@ -295,6 +358,7 @@ object RecipeImplTestData {
MIX_RECIPE_INSTRUCTION_RESPONSE_V1,
BOIL_RECIPE_INSTRUCTION_RESPONSE_V1
),
settings = GetRecipeSettingsResponseV1(disableAmount = true),
)
val MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V0 = AddRecipeInstructionV0("Mix the ingredients")

View File

@@ -0,0 +1,37 @@
package gq.kirmanak.mealient.ui.recipes.info
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters
@RunWith(Parameterized::class)
class MediantMethodTest(
private val input: Triple<Double, Int, Boolean>,
private val output: Triple<Int, Int, Int>,
) {
@Test
fun `when mediantMethod is called expect correct result`() {
assertThat(input.first.mediantMethod(input.second, input.third)).isEqualTo(output)
}
companion object {
@Parameters
@JvmStatic
fun parameters(): List<Array<Any>> {
return listOf(
arrayOf(Triple(0.333, 10, true), Triple(0, 1, 3)),
arrayOf(Triple(0.333, 10, false), Triple(0, 1, 3)),
arrayOf(Triple(0.333, 100, false), Triple(0, 1, 3)),
arrayOf(Triple(0.333, 100, true), Triple(0, 1, 3)),
arrayOf(Triple(1.5, 10, true), Triple(1, 1, 2)),
arrayOf(Triple(1.5, 10, false), Triple(0, 3, 2)),
arrayOf(Triple(0.4, 10, false), Triple(0, 2, 5)),
arrayOf(Triple(0.41412412412412, 100, true), Triple(0, 41, 99)),
arrayOf(Triple(8.98, 10, true), Triple(9, 0, 1)),
)
}
}
}

View File

@@ -3,7 +3,6 @@ package gq.kirmanak.mealient.ui.recipes.info
import androidx.lifecycle.asFlow
import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_CAKE_INFO_ENTITY
import io.mockk.coEvery
@@ -28,9 +27,8 @@ class RecipeInfoViewModelTest : BaseUnitTest() {
@Test
fun `when recipe is found then UI state has data`() = runTest {
val emptyNoteIngredient = RecipeIngredientEntity(recipeId = "42", note = "")
val returnedEntity = FULL_CAKE_INFO_ENTITY.copy(
recipeIngredients = FULL_CAKE_INFO_ENTITY.recipeIngredients + emptyNoteIngredient
recipeIngredients = FULL_CAKE_INFO_ENTITY.recipeIngredients
)
coEvery { recipeRepo.loadRecipeInfo(eq(RECIPE_ID)) } returns returnedEntity
val expected = RecipeInfoUiState(

View File

@@ -0,0 +1,185 @@
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "2383ca8fe2fbd04ddaec6d7680de62ad",
"entities": [
{
"tableName": "recipe_summaries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `description` TEXT NOT NULL, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "slug",
"columnName": "slug",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateUpdated",
"columnName": "date_updated",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "imageId",
"columnName": "image_id",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, `disable_amounts` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recipeYield",
"columnName": "recipe_yield",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "disableAmounts",
"columnName": "disable_amounts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_ingredient",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL, `food` TEXT, `unit` TEXT, `quantity` REAL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "note",
"columnName": "note",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "food",
"columnName": "food",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "unit",
"columnName": "unit",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "quantity",
"columnName": "quantity",
"affinity": "REAL",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_instruction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_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, '2383ca8fe2fbd04ddaec6d7680de62ad')"
]
}
}

View File

@@ -6,7 +6,7 @@ import gq.kirmanak.mealient.database.recipe.RecipeDao
import gq.kirmanak.mealient.database.recipe.entity.*
@Database(
version = 6,
version = 7,
entities = [
RecipeSummaryEntity::class,
RecipeEntity::class,
@@ -19,6 +19,7 @@ import gq.kirmanak.mealient.database.recipe.entity.*
AutoMigration(from = 3, to = 4),
AutoMigration(from = 4, to = 5, spec = AppDb.From4To5Migration::class),
AutoMigration(from = 5, to = 6, spec = AppDb.From5To6Migration::class),
AutoMigration(from = 6, to = 7),
]
)
@TypeConverters(RoomTypeConverters::class)

View File

@@ -8,4 +8,5 @@ import androidx.room.PrimaryKey
data class RecipeEntity(
@PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: String,
@ColumnInfo(name = "recipe_yield") val recipeYield: String,
@ColumnInfo(name = "disable_amounts", defaultValue = "true") val disableAmounts: Boolean,
)

View File

@@ -9,6 +9,9 @@ data class RecipeIngredientEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
@ColumnInfo(name = "recipe_id") val recipeId: String,
@ColumnInfo(name = "note") val note: String,
@ColumnInfo(name = "food") val food: String?,
@ColumnInfo(name = "unit") val unit: String?,
@ColumnInfo(name = "quantity") val quantity: Double?,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -18,6 +21,9 @@ data class RecipeIngredientEntity(
if (recipeId != other.recipeId) return false
if (note != other.note) return false
if (food != other.food) return false
if (unit != other.unit) return false
if (quantity != other.quantity) return false
return true
}
@@ -25,6 +31,9 @@ data class RecipeIngredientEntity(
override fun hashCode(): Int {
var result = recipeId.hashCode()
result = 31 * result + note.hashCode()
result = 31 * result + (food?.hashCode() ?: 0)
result = 31 * result + (unit?.hashCode() ?: 0)
result = 31 * result + quantity.hashCode()
return result
}
}

View File

@@ -8,13 +8,32 @@ data class GetRecipeResponseV1(
@SerialName("id") val remoteId: String,
@SerialName("name") val name: String,
@SerialName("recipeYield") val recipeYield: String = "",
@SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponseV1>,
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponseV1>,
@SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponseV1> = emptyList(),
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponseV1> = emptyList(),
@SerialName("settings") val settings: GetRecipeSettingsResponseV1,
)
@Serializable
data class GetRecipeSettingsResponseV1(
@SerialName("disableAmount") val disableAmount: Boolean,
)
@Serializable
data class GetRecipeIngredientResponseV1(
@SerialName("note") val note: String = "",
@SerialName("unit") val unit: GetRecipeIngredientUnitResponseV1?,
@SerialName("food") val food: GetRecipeIngredientFoodResponseV1?,
@SerialName("quantity") val quantity: Double?,
)
@Serializable
data class GetRecipeIngredientFoodResponseV1(
@SerialName("name") val name: String = "",
)
@Serializable
data class GetRecipeIngredientUnitResponseV1(
@SerialName("name") val name: String = "",
)
@Serializable