Add linked ingredients to recipe step (#177)
* Add Compose to app module * Move Theme to ui module * Add Coil image loader * Use Compose for recipe screen * Save instruction to ingredient relation to DB * Display ingredients as server formats them * Display linked ingredients under each step * Fix ingredients padding * Show recipe full screen * Fix recipe screen UI issues * Hide keyboard on recipe navigation * Fix loading recipes from DB with no instructions or ingredients * Add instructions section title * Add ingredients section title * Remove unused view holders
This commit is contained in:
@@ -5,12 +5,13 @@ import gq.kirmanak.mealient.database.recipe.RecipeDao
|
||||
import gq.kirmanak.mealient.database.recipe.entity.*
|
||||
|
||||
@Database(
|
||||
version = 10,
|
||||
version = 11,
|
||||
entities = [
|
||||
RecipeSummaryEntity::class,
|
||||
RecipeEntity::class,
|
||||
RecipeIngredientEntity::class,
|
||||
RecipeInstructionEntity::class,
|
||||
RecipeIngredientToInstructionEntity::class,
|
||||
]
|
||||
)
|
||||
@TypeConverters(RoomTypeConverters::class)
|
||||
|
||||
@@ -35,13 +35,17 @@ internal interface RecipeDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecipeIngredients(ingredients: List<RecipeIngredientEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertIngredientToInstructionEntities(entities: List<RecipeIngredientToInstructionEntity>)
|
||||
|
||||
@Transaction
|
||||
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) // The lint is wrong, the columns are actually used
|
||||
@Query(
|
||||
"SELECT * FROM recipe " +
|
||||
"JOIN recipe_summaries USING(recipe_id) " +
|
||||
"JOIN recipe_ingredient USING(recipe_id) " +
|
||||
"JOIN recipe_instruction USING(recipe_id) " +
|
||||
"LEFT JOIN recipe_ingredient USING(recipe_id) " +
|
||||
"LEFT JOIN recipe_instruction USING(recipe_id) " +
|
||||
"LEFT JOIN recipe_ingredient_to_instruction USING(recipe_id) " +
|
||||
"WHERE recipe.recipe_id = :recipeId"
|
||||
)
|
||||
suspend fun queryFullRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions?
|
||||
@@ -52,6 +56,9 @@ internal interface RecipeDao {
|
||||
@Query("DELETE FROM recipe_instruction WHERE recipe_id IN (:recipeIds)")
|
||||
suspend fun deleteRecipeInstructions(vararg recipeIds: String)
|
||||
|
||||
@Query("DELETE FROM recipe_ingredient_to_instruction WHERE recipe_id IN (:recipeIds)")
|
||||
suspend fun deleteRecipeIngredientToInstructions(vararg recipeIds: String)
|
||||
|
||||
@Query("UPDATE recipe_summaries SET recipe_summaries_is_favorite = 1 WHERE recipe_summaries_slug IN (:favorites)")
|
||||
suspend fun setFavorite(favorites: List<String>)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package gq.kirmanak.mealient.database.recipe
|
||||
import androidx.paging.PagingSource
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientToInstructionEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions
|
||||
@@ -19,7 +20,8 @@ interface RecipeStorage {
|
||||
suspend fun saveRecipeInfo(
|
||||
recipe: RecipeEntity,
|
||||
ingredients: List<RecipeIngredientEntity>,
|
||||
instructions: List<RecipeInstructionEntity>
|
||||
instructions: List<RecipeInstructionEntity>,
|
||||
ingredientToInstruction: List<RecipeIngredientToInstructionEntity>,
|
||||
)
|
||||
|
||||
suspend fun queryRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions?
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.room.withTransaction
|
||||
import gq.kirmanak.mealient.database.AppDb
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientToInstructionEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions
|
||||
@@ -44,9 +45,10 @@ internal class RecipeStorageImpl @Inject constructor(
|
||||
override suspend fun saveRecipeInfo(
|
||||
recipe: RecipeEntity,
|
||||
ingredients: List<RecipeIngredientEntity>,
|
||||
instructions: List<RecipeInstructionEntity>
|
||||
instructions: List<RecipeInstructionEntity>,
|
||||
ingredientToInstruction: List<RecipeIngredientToInstructionEntity>,
|
||||
) {
|
||||
logger.v { "saveRecipeInfo() called with: recipe = $recipe" }
|
||||
logger.v { "saveRecipeInfo() called with: recipe = $recipe, ingredients = $ingredients, instructions = $instructions, ingredientToInstructions = $ingredientToInstruction" }
|
||||
db.withTransaction {
|
||||
recipeDao.insertRecipe(recipe)
|
||||
|
||||
@@ -55,6 +57,9 @@ internal class RecipeStorageImpl @Inject constructor(
|
||||
|
||||
recipeDao.deleteRecipeInstructions(recipe.remoteId)
|
||||
recipeDao.insertRecipeInstructions(instructions)
|
||||
|
||||
recipeDao.deleteRecipeIngredientToInstructions(recipe.remoteId)
|
||||
recipeDao.insertIngredientToInstructionEntities(ingredientToInstruction)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,37 +17,12 @@ import androidx.room.PrimaryKey
|
||||
]
|
||||
)
|
||||
data class RecipeIngredientEntity(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "recipe_ingredient_local_id") val localId: Long = 0,
|
||||
@PrimaryKey @ColumnInfo(name = "recipe_ingredient_id") val id: String,
|
||||
@ColumnInfo(name = "recipe_id", index = true) val recipeId: String,
|
||||
@ColumnInfo(name = "recipe_ingredient_note") val note: String,
|
||||
@ColumnInfo(name = "recipe_ingredient_food") val food: String?,
|
||||
@ColumnInfo(name = "recipe_ingredient_unit") val unit: String?,
|
||||
@ColumnInfo(name = "recipe_ingredient_quantity") val quantity: Double?,
|
||||
@ColumnInfo(name = "recipe_ingredient_display") val display: String,
|
||||
@ColumnInfo(name = "recipe_ingredient_title") val title: String?,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as 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
|
||||
if (title != other.title) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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() ?: 0)
|
||||
result = 31 * result + (title?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
package gq.kirmanak.mealient.database.recipe.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
|
||||
@Entity(
|
||||
tableName = "recipe_ingredient_to_instruction",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = RecipeEntity::class,
|
||||
parentColumns = ["recipe_id"],
|
||||
childColumns = ["recipe_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = RecipeIngredientEntity::class,
|
||||
parentColumns = ["recipe_ingredient_id"],
|
||||
childColumns = ["ingredient_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = RecipeInstructionEntity::class,
|
||||
parentColumns = ["recipe_instruction_id"],
|
||||
childColumns = ["instruction_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
],
|
||||
primaryKeys = ["recipe_id", "ingredient_id", "instruction_id"]
|
||||
)
|
||||
data class RecipeIngredientToInstructionEntity(
|
||||
@ColumnInfo(name = "recipe_id", index = true) val recipeId: String,
|
||||
@ColumnInfo(name = "ingredient_id", index = true) val ingredientId: String,
|
||||
@ColumnInfo(name = "instruction_id", index = true) val instructionId: String,
|
||||
)
|
||||
@@ -17,25 +17,8 @@ import androidx.room.PrimaryKey
|
||||
]
|
||||
)
|
||||
data class RecipeInstructionEntity(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "recipe_instruction_local_id") val localId: Long = 0,
|
||||
@PrimaryKey @ColumnInfo(name = "recipe_instruction_id") val id: String,
|
||||
@ColumnInfo(name = "recipe_id", index = true) val recipeId: String,
|
||||
@ColumnInfo(name = "recipe_instruction_text") val text: String,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as RecipeInstructionEntity
|
||||
|
||||
if (recipeId != other.recipeId) return false
|
||||
if (text != other.text) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = recipeId.hashCode()
|
||||
result = 31 * result + text.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@ColumnInfo(name = "recipe_instruction_title") val title: String?,
|
||||
)
|
||||
|
||||
@@ -15,8 +15,5 @@ data class RecipeSummaryEntity(
|
||||
@ColumnInfo(name = "recipe_summaries_date_added") val dateAdded: LocalDate,
|
||||
@ColumnInfo(name = "recipe_summaries_date_updated") val dateUpdated: LocalDateTime,
|
||||
@ColumnInfo(name = "recipe_summaries_image_id") val imageId: String?,
|
||||
@ColumnInfo(
|
||||
name = "recipe_summaries_is_favorite",
|
||||
defaultValue = "false"
|
||||
) val isFavorite: Boolean,
|
||||
@ColumnInfo(name = "recipe_summaries_is_favorite") val isFavorite: Boolean,
|
||||
)
|
||||
@@ -20,4 +20,9 @@ data class RecipeWithSummaryAndIngredientsAndInstructions(
|
||||
entityColumn = "recipe_id"
|
||||
)
|
||||
val recipeInstructions: List<RecipeInstructionEntity>,
|
||||
@Relation(
|
||||
parentColumn = "recipe_id",
|
||||
entityColumn = "recipe_id"
|
||||
)
|
||||
val recipeIngredientToInstructionEntity: List<RecipeIngredientToInstructionEntity>,
|
||||
)
|
||||
|
||||
@@ -50,7 +50,11 @@ internal class RecipeStorageImplTest : HiltRobolectricTest() {
|
||||
subject.saveRecipeInfo(
|
||||
CAKE_RECIPE_ENTITY,
|
||||
listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY),
|
||||
listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY)
|
||||
listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY),
|
||||
listOf(
|
||||
MIX_SUGAR_RECIPE_INGREDIENT_INSTRUCTION_ENTITY,
|
||||
MIX_BREAD_RECIPE_INGREDIENT_INSTRUCTION_ENTITY
|
||||
),
|
||||
)
|
||||
val actual = recipeDao.queryFullRecipeInfo("1")
|
||||
assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY)
|
||||
@@ -63,11 +67,16 @@ internal class RecipeStorageImplTest : HiltRobolectricTest() {
|
||||
CAKE_RECIPE_ENTITY,
|
||||
listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY),
|
||||
listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY),
|
||||
listOf(
|
||||
MIX_SUGAR_RECIPE_INGREDIENT_INSTRUCTION_ENTITY,
|
||||
MIX_BREAD_RECIPE_INGREDIENT_INSTRUCTION_ENTITY
|
||||
),
|
||||
)
|
||||
subject.saveRecipeInfo(
|
||||
PORRIDGE_RECIPE_ENTITY_FULL,
|
||||
listOf(PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY, PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY),
|
||||
listOf(PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY, PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY),
|
||||
emptyList(),
|
||||
)
|
||||
val actual = recipeDao.queryFullRecipeInfo("2")
|
||||
assertThat(actual).isEqualTo(FULL_PORRIDGE_INFO_ENTITY)
|
||||
@@ -80,14 +89,19 @@ internal class RecipeStorageImplTest : HiltRobolectricTest() {
|
||||
CAKE_RECIPE_ENTITY,
|
||||
listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY),
|
||||
listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY),
|
||||
listOf(
|
||||
MIX_SUGAR_RECIPE_INGREDIENT_INSTRUCTION_ENTITY,
|
||||
MIX_BREAD_RECIPE_INGREDIENT_INSTRUCTION_ENTITY
|
||||
),
|
||||
)
|
||||
subject.saveRecipeInfo(
|
||||
CAKE_RECIPE_ENTITY,
|
||||
listOf(CAKE_BREAD_RECIPE_INGREDIENT_ENTITY),
|
||||
listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY),
|
||||
emptyList(),
|
||||
)
|
||||
val actual = recipeDao.queryFullRecipeInfo("1")?.recipeIngredients
|
||||
val expected = listOf(CAKE_BREAD_RECIPE_INGREDIENT_ENTITY.copy(localId = 3))
|
||||
val expected = listOf(CAKE_BREAD_RECIPE_INGREDIENT_ENTITY)
|
||||
assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@@ -98,14 +112,19 @@ internal class RecipeStorageImplTest : HiltRobolectricTest() {
|
||||
CAKE_RECIPE_ENTITY,
|
||||
listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY),
|
||||
listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY),
|
||||
listOf(
|
||||
MIX_SUGAR_RECIPE_INGREDIENT_INSTRUCTION_ENTITY,
|
||||
MIX_BREAD_RECIPE_INGREDIENT_INSTRUCTION_ENTITY
|
||||
),
|
||||
)
|
||||
subject.saveRecipeInfo(
|
||||
CAKE_RECIPE_ENTITY,
|
||||
listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY),
|
||||
listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY),
|
||||
emptyList(),
|
||||
)
|
||||
val actual = recipeDao.queryFullRecipeInfo("1")?.recipeInstructions
|
||||
val expected = listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY.copy(localId = 3))
|
||||
val expected = listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY)
|
||||
assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user