Replace "Mealie" with "Mealient" everywhere
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
package gq.kirmanak.mealient.data.recipes
|
||||
|
||||
import android.widget.ImageView
|
||||
|
||||
interface RecipeImageLoader {
|
||||
suspend fun loadRecipeImage(view: ImageView, slug: String?)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package gq.kirmanak.mealient.data.recipes
|
||||
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
|
||||
import gq.kirmanak.mealient.data.recipes.db.RecipeStorageImpl
|
||||
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageLoaderImpl
|
||||
import gq.kirmanak.mealient.data.recipes.impl.RecipeRepoImpl
|
||||
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
||||
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSourceImpl
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
|
||||
@ExperimentalPagingApi
|
||||
@ExperimentalSerializationApi
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface RecipeModule {
|
||||
@Binds
|
||||
fun provideRecipeDataSource(recipeDataSourceImpl: RecipeDataSourceImpl): RecipeDataSource
|
||||
|
||||
@Binds
|
||||
fun provideRecipeStorage(recipeStorageImpl: RecipeStorageImpl): RecipeStorage
|
||||
|
||||
@Binds
|
||||
fun provideRecipeRepo(recipeRepoImpl: RecipeRepoImpl): RecipeRepo
|
||||
|
||||
@Binds
|
||||
fun provideRecipeImageLoader(recipeImageLoaderImpl: RecipeImageLoaderImpl): RecipeImageLoader
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package gq.kirmanak.mealient.data.recipes
|
||||
|
||||
import androidx.paging.Pager
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo
|
||||
|
||||
interface RecipeRepo {
|
||||
fun createPager(): Pager<Int, RecipeSummaryEntity>
|
||||
|
||||
suspend fun clearLocalData()
|
||||
|
||||
suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package gq.kirmanak.mealient.data.recipes.db
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.*
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.*
|
||||
import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo
|
||||
|
||||
@Dao
|
||||
interface RecipeDao {
|
||||
@Query("SELECT * FROM tags")
|
||||
suspend fun queryAllTags(): List<TagEntity>
|
||||
|
||||
@Query("SELECT * FROM categories")
|
||||
suspend fun queryAllCategories(): List<CategoryEntity>
|
||||
|
||||
@Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC")
|
||||
fun queryRecipesByPages(): PagingSource<Int, RecipeSummaryEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecipe(recipeSummaryEntity: RecipeSummaryEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertTag(tagEntity: TagEntity): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertTagRecipeEntity(tagRecipeEntity: TagRecipeEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertCategory(categoryEntity: CategoryEntity): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertCategoryRecipeEntity(categoryRecipeEntity: CategoryRecipeEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertTagRecipeEntities(tagRecipeEntities: Set<TagRecipeEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertCategoryRecipeEntities(categoryRecipeEntities: Set<CategoryRecipeEntity>)
|
||||
|
||||
@Query("DELETE FROM recipe_summaries")
|
||||
suspend fun removeAllRecipes()
|
||||
|
||||
@Query("DELETE FROM tags")
|
||||
suspend fun removeAllTags()
|
||||
|
||||
@Query("DELETE FROM categories")
|
||||
suspend fun removeAllCategories()
|
||||
|
||||
@Query("SELECT * FROM recipe_summaries ORDER BY date_updated DESC")
|
||||
suspend fun queryAllRecipes(): List<RecipeSummaryEntity>
|
||||
|
||||
@Query("SELECT * FROM category_recipe")
|
||||
suspend fun queryAllCategoryRecipes(): List<CategoryRecipeEntity>
|
||||
|
||||
@Query("SELECT * FROM tag_recipe")
|
||||
suspend fun queryAllTagRecipes(): List<TagRecipeEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecipe(recipe: RecipeEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecipeInstructions(instructions: List<RecipeInstructionEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecipeIngredients(ingredients: List<RecipeIngredientEntity>)
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM recipe JOIN recipe_summaries ON recipe.remote_id = recipe_summaries.remote_id JOIN recipe_ingredient ON recipe_ingredient.recipe_id = recipe.remote_id JOIN recipe_instruction ON recipe_instruction.recipe_id = recipe.remote_id WHERE recipe.remote_id = :recipeId")
|
||||
suspend fun queryFullRecipeInfo(recipeId: Long): FullRecipeInfo?
|
||||
|
||||
@Query("DELETE FROM recipe_ingredient WHERE recipe_id = :recipeId")
|
||||
suspend fun deleteRecipeIngredients(recipeId: Long)
|
||||
|
||||
@Query("DELETE FROM recipe_instruction WHERE recipe_id = :recipeId")
|
||||
suspend fun deleteRecipeInstructions(recipeId: Long)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package gq.kirmanak.mealient.data.recipes.db
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||
|
||||
interface RecipeStorage {
|
||||
suspend fun saveRecipes(recipes: List<GetRecipeSummaryResponse>)
|
||||
|
||||
fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity>
|
||||
|
||||
suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>)
|
||||
|
||||
suspend fun clearAllLocalData()
|
||||
|
||||
suspend fun saveRecipeInfo(recipe: GetRecipeResponse)
|
||||
|
||||
suspend fun queryRecipeInfo(recipeId: Long): FullRecipeInfo
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package gq.kirmanak.mealient.data.recipes.db
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.withTransaction
|
||||
import gq.kirmanak.mealient.data.AppDb
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.*
|
||||
import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeIngredientResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeInstructionResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class RecipeStorageImpl @Inject constructor(
|
||||
private val db: AppDb
|
||||
) : RecipeStorage {
|
||||
private val recipeDao: RecipeDao by lazy { db.recipeDao() }
|
||||
|
||||
override suspend fun saveRecipes(
|
||||
recipes: List<GetRecipeSummaryResponse>
|
||||
) = db.withTransaction {
|
||||
Timber.v("saveRecipes() called with $recipes")
|
||||
|
||||
val tagEntities = mutableSetOf<TagEntity>()
|
||||
tagEntities.addAll(recipeDao.queryAllTags())
|
||||
|
||||
val categoryEntities = mutableSetOf<CategoryEntity>()
|
||||
categoryEntities.addAll(recipeDao.queryAllCategories())
|
||||
|
||||
val tagRecipeEntities = mutableSetOf<TagRecipeEntity>()
|
||||
val categoryRecipeEntities = mutableSetOf<CategoryRecipeEntity>()
|
||||
|
||||
for (recipe in recipes) {
|
||||
val recipeSummaryEntity = recipe.recipeEntity()
|
||||
recipeDao.insertRecipe(recipeSummaryEntity)
|
||||
|
||||
for (tag in recipe.tags) {
|
||||
val tagId = getIdOrInsert(tagEntities, tag)
|
||||
tagRecipeEntities += TagRecipeEntity(tagId, recipeSummaryEntity.remoteId)
|
||||
}
|
||||
|
||||
for (category in recipe.recipeCategories) {
|
||||
val categoryId = getOrInsert(categoryEntities, category)
|
||||
categoryRecipeEntities += CategoryRecipeEntity(
|
||||
categoryId,
|
||||
recipeSummaryEntity.remoteId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
recipeDao.insertTagRecipeEntities(tagRecipeEntities)
|
||||
recipeDao.insertCategoryRecipeEntities(categoryRecipeEntities)
|
||||
}
|
||||
|
||||
private suspend fun getOrInsert(
|
||||
categoryEntities: MutableSet<CategoryEntity>,
|
||||
category: String
|
||||
): Long {
|
||||
val existingCategory = categoryEntities.find { it.name == category }
|
||||
val categoryId = if (existingCategory == null) {
|
||||
val categoryEntity = CategoryEntity(name = category)
|
||||
val newId = recipeDao.insertCategory(categoryEntity)
|
||||
categoryEntities.add(categoryEntity.copy(localId = newId))
|
||||
newId
|
||||
} else {
|
||||
existingCategory.localId
|
||||
}
|
||||
return categoryId
|
||||
}
|
||||
|
||||
private suspend fun getIdOrInsert(
|
||||
tagEntities: MutableSet<TagEntity>,
|
||||
tag: String
|
||||
): Long {
|
||||
val existingTag = tagEntities.find { it.name == tag }
|
||||
val tagId = if (existingTag == null) {
|
||||
val tagEntity = TagEntity(name = tag)
|
||||
val newId = recipeDao.insertTag(tagEntity)
|
||||
tagEntities.add(tagEntity.copy(localId = newId))
|
||||
newId
|
||||
} else {
|
||||
existingTag.localId
|
||||
}
|
||||
return tagId
|
||||
}
|
||||
|
||||
private fun GetRecipeSummaryResponse.recipeEntity() = RecipeSummaryEntity(
|
||||
remoteId = remoteId,
|
||||
name = name,
|
||||
slug = slug,
|
||||
image = image,
|
||||
description = description,
|
||||
rating = rating,
|
||||
dateAdded = dateAdded,
|
||||
dateUpdated = dateUpdated,
|
||||
)
|
||||
|
||||
override fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity> {
|
||||
Timber.v("queryRecipes() called")
|
||||
return recipeDao.queryRecipesByPages()
|
||||
}
|
||||
|
||||
override suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>) {
|
||||
Timber.v("refreshAll() called with: recipes = $recipes")
|
||||
db.withTransaction {
|
||||
recipeDao.removeAllRecipes()
|
||||
saveRecipes(recipes)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearAllLocalData() {
|
||||
Timber.v("clearAllLocalData() called")
|
||||
db.withTransaction {
|
||||
recipeDao.removeAllRecipes()
|
||||
recipeDao.removeAllCategories()
|
||||
recipeDao.removeAllTags()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveRecipeInfo(recipe: GetRecipeResponse) {
|
||||
Timber.v("saveRecipeInfo() called with: recipe = $recipe")
|
||||
db.withTransaction {
|
||||
recipeDao.insertRecipe(recipe.toRecipeEntity())
|
||||
|
||||
recipeDao.deleteRecipeIngredients(recipe.remoteId)
|
||||
val ingredients = recipe.recipeIngredients.map {
|
||||
it.toRecipeIngredientEntity(recipe.remoteId)
|
||||
}
|
||||
recipeDao.insertRecipeIngredients(ingredients)
|
||||
|
||||
recipeDao.deleteRecipeInstructions(recipe.remoteId)
|
||||
val instructions = recipe.recipeInstructions.map {
|
||||
it.toRecipeInstructionEntity(recipe.remoteId)
|
||||
}
|
||||
recipeDao.insertRecipeInstructions(instructions)
|
||||
}
|
||||
}
|
||||
|
||||
private fun GetRecipeResponse.toRecipeEntity() = RecipeEntity(
|
||||
remoteId = remoteId,
|
||||
recipeYield = recipeYield
|
||||
)
|
||||
|
||||
private fun GetRecipeIngredientResponse.toRecipeIngredientEntity(remoteId: Long) =
|
||||
RecipeIngredientEntity(
|
||||
recipeId = remoteId,
|
||||
title = title,
|
||||
note = note,
|
||||
unit = unit,
|
||||
food = food,
|
||||
disableAmount = disableAmount,
|
||||
quantity = quantity
|
||||
)
|
||||
|
||||
private fun GetRecipeInstructionResponse.toRecipeInstructionEntity(remoteId: Long) =
|
||||
RecipeInstructionEntity(
|
||||
recipeId = remoteId,
|
||||
title = title,
|
||||
text = text
|
||||
)
|
||||
|
||||
override suspend fun queryRecipeInfo(recipeId: Long): FullRecipeInfo {
|
||||
Timber.v("queryRecipeInfo() called with: recipeId = $recipeId")
|
||||
val fullRecipeInfo = checkNotNull(recipeDao.queryFullRecipeInfo(recipeId)) {
|
||||
"Can't find recipe by id $recipeId in DB"
|
||||
}
|
||||
Timber.v("queryRecipeInfo() returned: $fullRecipeInfo")
|
||||
return fullRecipeInfo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package gq.kirmanak.mealient.data.recipes.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "categories", indices = [Index(value = ["name"], unique = true)])
|
||||
data class CategoryEntity(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
package gq.kirmanak.mealient.data.recipes.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
|
||||
@Entity(
|
||||
tableName = "category_recipe",
|
||||
primaryKeys = ["category_id", "recipe_id"],
|
||||
indices = [Index(value = ["category_id", "recipe_id"], unique = true)],
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = CategoryEntity::class,
|
||||
parentColumns = ["local_id"],
|
||||
childColumns = ["category_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE
|
||||
), ForeignKey(
|
||||
entity = RecipeSummaryEntity::class,
|
||||
parentColumns = ["remote_id"],
|
||||
childColumns = ["recipe_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE
|
||||
)]
|
||||
)
|
||||
data class CategoryRecipeEntity(
|
||||
@ColumnInfo(name = "category_id") val categoryId: Long,
|
||||
@ColumnInfo(name = "recipe_id", index = true) val recipeId: Long
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package gq.kirmanak.mealient.data.recipes.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "recipe")
|
||||
data class RecipeEntity(
|
||||
@PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: Long,
|
||||
@ColumnInfo(name = "recipe_yield") val recipeYield: String,
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package gq.kirmanak.mealient.data.recipes.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "recipe_ingredient")
|
||||
data class RecipeIngredientEntity(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
|
||||
@ColumnInfo(name = "recipe_id") val recipeId: Long,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "note") val note: String,
|
||||
@ColumnInfo(name = "unit") val unit: String,
|
||||
@ColumnInfo(name = "food") val food: String,
|
||||
@ColumnInfo(name = "disable_amount") val disableAmount: Boolean,
|
||||
@ColumnInfo(name = "quantity") val quantity: Int,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package gq.kirmanak.mealient.data.recipes.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "recipe_instruction")
|
||||
data class RecipeInstructionEntity(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
|
||||
@ColumnInfo(name = "recipe_id") val recipeId: Long,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "text") val text: String,
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package gq.kirmanak.mealient.data.recipes.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
|
||||
@Entity(tableName = "recipe_summaries")
|
||||
data class RecipeSummaryEntity(
|
||||
@PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: Long,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "slug") val slug: String,
|
||||
@ColumnInfo(name = "image") val image: String,
|
||||
@ColumnInfo(name = "description") val description: String,
|
||||
@ColumnInfo(name = "rating") val rating: Int?,
|
||||
@ColumnInfo(name = "date_added") val dateAdded: LocalDate,
|
||||
@ColumnInfo(name = "date_updated") val dateUpdated: LocalDateTime
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "RecipeEntity(remoteId=$remoteId, name='$name')"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package gq.kirmanak.mealient.data.recipes.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "tags", indices = [Index(value = ["name"], unique = true)])
|
||||
data class TagEntity(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
|
||||
@ColumnInfo(name = "name") val name: String
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
package gq.kirmanak.mealient.data.recipes.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
|
||||
@Entity(
|
||||
tableName = "tag_recipe",
|
||||
primaryKeys = ["tag_id", "recipe_id"],
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = TagEntity::class,
|
||||
parentColumns = ["local_id"],
|
||||
childColumns = ["tag_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE
|
||||
), ForeignKey(
|
||||
entity = RecipeSummaryEntity::class,
|
||||
parentColumns = ["remote_id"],
|
||||
childColumns = ["recipe_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE
|
||||
)]
|
||||
)
|
||||
data class TagRecipeEntity(
|
||||
@ColumnInfo(name = "tag_id") val tagId: Long,
|
||||
@ColumnInfo(name = "recipe_id", index = true) val recipeId: Long
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
package gq.kirmanak.mealient.data.recipes.impl
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeEntity
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeIngredientEntity
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeInstructionEntity
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
|
||||
data class FullRecipeInfo(
|
||||
@Embedded val recipeEntity: RecipeEntity,
|
||||
@Relation(
|
||||
parentColumn = "remote_id",
|
||||
entityColumn = "remote_id"
|
||||
)
|
||||
val recipeSummaryEntity: RecipeSummaryEntity,
|
||||
@Relation(
|
||||
parentColumn = "remote_id",
|
||||
entityColumn = "recipe_id"
|
||||
)
|
||||
val recipeIngredients: List<RecipeIngredientEntity>,
|
||||
@Relation(
|
||||
parentColumn = "remote_id",
|
||||
entityColumn = "recipe_id"
|
||||
)
|
||||
val recipeInstructions: List<RecipeInstructionEntity>,
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
package gq.kirmanak.mealient.data.recipes.impl
|
||||
|
||||
import android.widget.ImageView
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeImageLoader
|
||||
import gq.kirmanak.mealient.ui.ImageLoader
|
||||
import javax.inject.Inject
|
||||
|
||||
class RecipeImageLoaderImpl @Inject constructor(
|
||||
private val imageLoader: ImageLoader,
|
||||
private val authRepo: AuthRepo
|
||||
): RecipeImageLoader {
|
||||
override suspend fun loadRecipeImage(view: ImageView, slug: String?) {
|
||||
val baseUrl = authRepo.getBaseUrl()
|
||||
val recipeImageUrl =
|
||||
if (baseUrl.isNullOrBlank() || slug.isNullOrBlank()) null
|
||||
else "$baseUrl/api/media/recipes/$slug/images/original.webp"
|
||||
imageLoader.loadImage(recipeImageUrl, R.drawable.placeholder_recipe, view)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package gq.kirmanak.mealient.data.recipes.impl
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class RecipePagingSourceFactory @Inject constructor(
|
||||
private val recipeStorage: RecipeStorage
|
||||
) : () -> PagingSource<Int, RecipeSummaryEntity> {
|
||||
private val sources: MutableList<PagingSource<Int, RecipeSummaryEntity>> = mutableListOf()
|
||||
|
||||
override fun invoke(): PagingSource<Int, RecipeSummaryEntity> {
|
||||
Timber.v("invoke() called")
|
||||
val newSource = recipeStorage.queryRecipes()
|
||||
sources.add(newSource)
|
||||
return newSource
|
||||
}
|
||||
|
||||
fun invalidate() {
|
||||
Timber.v("invalidate() called")
|
||||
for (source in sources) {
|
||||
if (!source.invalid) {
|
||||
source.invalidate()
|
||||
}
|
||||
}
|
||||
sources.removeAll { it.invalid }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package gq.kirmanak.mealient.data.recipes.impl
|
||||
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ExperimentalPagingApi
|
||||
class RecipeRepoImpl @Inject constructor(
|
||||
private val mediator: RecipesRemoteMediator,
|
||||
private val storage: RecipeStorage,
|
||||
private val pagingSourceFactory: RecipePagingSourceFactory,
|
||||
private val dataSource: RecipeDataSource,
|
||||
) : RecipeRepo {
|
||||
override fun createPager(): Pager<Int, RecipeSummaryEntity> {
|
||||
Timber.v("createPager() called")
|
||||
val pagingConfig = PagingConfig(pageSize = 30, enablePlaceholders = true)
|
||||
return Pager(
|
||||
config = pagingConfig,
|
||||
remoteMediator = mediator,
|
||||
pagingSourceFactory = pagingSourceFactory
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun clearLocalData() {
|
||||
Timber.v("clearLocalData() called")
|
||||
storage.clearAllLocalData()
|
||||
}
|
||||
|
||||
override suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo {
|
||||
Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug")
|
||||
|
||||
try {
|
||||
storage.saveRecipeInfo(dataSource.requestRecipeInfo(recipeSlug))
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "loadRecipeInfo: can't update full recipe info")
|
||||
}
|
||||
|
||||
return storage.queryRecipeInfo(recipeId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package gq.kirmanak.mealient.data.recipes.impl
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadType
|
||||
import androidx.paging.LoadType.PREPEND
|
||||
import androidx.paging.LoadType.REFRESH
|
||||
import androidx.paging.PagingState
|
||||
import androidx.paging.RemoteMediator
|
||||
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ExperimentalPagingApi
|
||||
class RecipesRemoteMediator @Inject constructor(
|
||||
private val storage: RecipeStorage,
|
||||
private val network: RecipeDataSource,
|
||||
private val pagingSourceFactory: RecipePagingSourceFactory,
|
||||
) : RemoteMediator<Int, RecipeSummaryEntity>() {
|
||||
|
||||
@VisibleForTesting
|
||||
var lastRequestEnd: Int = 0
|
||||
|
||||
override suspend fun load(
|
||||
loadType: LoadType,
|
||||
state: PagingState<Int, RecipeSummaryEntity>
|
||||
): MediatorResult {
|
||||
Timber.v("load() called with: lastRequestEnd = $lastRequestEnd, loadType = $loadType, state = $state")
|
||||
|
||||
if (loadType == PREPEND) {
|
||||
Timber.i("load: early exit, PREPEND isn't supported")
|
||||
return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
|
||||
val start = if (loadType == REFRESH) 0 else lastRequestEnd
|
||||
val limit = if (loadType == REFRESH) state.config.initialLoadSize else state.config.pageSize
|
||||
|
||||
val count: Int = try {
|
||||
val recipes = network.requestRecipes(start, limit)
|
||||
if (loadType == REFRESH) storage.refreshAll(recipes)
|
||||
else storage.saveRecipes(recipes)
|
||||
recipes.size
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Can't load recipes")
|
||||
return MediatorResult.Error(e)
|
||||
}
|
||||
|
||||
// After something is inserted into DB the paging sources have to be invalidated
|
||||
// But for some reason Room/Paging library don't do it automatically
|
||||
// Here we invalidate them manually.
|
||||
// Read that trick here https://github.com/android/architecture-components-samples/issues/889#issuecomment-880847858
|
||||
pagingSourceFactory.invalidate()
|
||||
|
||||
Timber.d("load: expectedCount = $limit, received $count")
|
||||
lastRequestEnd = start + count
|
||||
return MediatorResult.Success(endOfPaginationReached = count < limit)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network
|
||||
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||
|
||||
interface RecipeDataSource {
|
||||
suspend fun requestRecipes(start: Int = 0, limit: Int = 9999): List<GetRecipeSummaryResponse>
|
||||
|
||||
suspend fun requestRecipeInfo(slug: String): GetRecipeResponse
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network
|
||||
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.impl.RetrofitBuilder
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ExperimentalSerializationApi
|
||||
class RecipeDataSourceImpl @Inject constructor(
|
||||
private val authRepo: AuthRepo,
|
||||
private val retrofitBuilder: RetrofitBuilder
|
||||
) : RecipeDataSource {
|
||||
private var _recipeService: RecipeService? = null
|
||||
|
||||
override suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponse> {
|
||||
Timber.v("requestRecipes() called with: start = $start, limit = $limit")
|
||||
val service: RecipeService = getRecipeService()
|
||||
val recipeSummary = service.getRecipeSummary(start, limit)
|
||||
Timber.v("requestRecipes() returned: $recipeSummary")
|
||||
return recipeSummary
|
||||
}
|
||||
|
||||
override suspend fun requestRecipeInfo(slug: String): GetRecipeResponse {
|
||||
Timber.v("requestRecipeInfo() called with: slug = $slug")
|
||||
val service: RecipeService = getRecipeService()
|
||||
val recipeInfo = service.getRecipe(slug)
|
||||
Timber.v("requestRecipeInfo() returned: $recipeInfo")
|
||||
return recipeInfo
|
||||
}
|
||||
|
||||
private suspend fun getRecipeService(): RecipeService {
|
||||
Timber.v("getRecipeService() called")
|
||||
val cachedService: RecipeService? = _recipeService
|
||||
val service: RecipeService = if (cachedService == null) {
|
||||
val baseUrl = checkNotNull(authRepo.getBaseUrl()) { "Base url is null" }
|
||||
val token = checkNotNull(authRepo.getToken()) { "Token is null" }
|
||||
Timber.d("requestRecipes: baseUrl = $baseUrl, token = $token")
|
||||
val retrofit = retrofitBuilder.buildRetrofit(baseUrl, token)
|
||||
val createdService = retrofit.create(RecipeService::class.java)
|
||||
_recipeService = createdService
|
||||
createdService
|
||||
} else {
|
||||
cachedService
|
||||
}
|
||||
return service
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network
|
||||
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface RecipeService {
|
||||
@GET("/api/recipes/summary")
|
||||
suspend fun getRecipeSummary(
|
||||
@Query("start") start: Int,
|
||||
@Query("limit") limit: Int
|
||||
): List<GetRecipeSummaryResponse>
|
||||
|
||||
@GET("/api/recipes/{recipe_slug}")
|
||||
suspend fun getRecipe(
|
||||
@Path("recipe_slug") recipeSlug: String
|
||||
): GetRecipeResponse
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network.response
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeIngredientResponse(
|
||||
@SerialName("title") val title: String = "",
|
||||
@SerialName("note") val note: String = "",
|
||||
@SerialName("unit") val unit: String = "",
|
||||
@SerialName("food") val food: String = "",
|
||||
@SerialName("disableAmount") val disableAmount: Boolean,
|
||||
@SerialName("quantity") val quantity: Int,
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network.response
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeInstructionResponse(
|
||||
@SerialName("title") val title: String = "",
|
||||
@SerialName("text") val text: String,
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network.response
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeResponse(
|
||||
@SerialName("id") val remoteId: Long,
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("slug") val slug: String,
|
||||
@SerialName("image") val image: String,
|
||||
@SerialName("description") val description: String = "",
|
||||
@SerialName("recipeCategory") val recipeCategories: List<String>,
|
||||
@SerialName("tags") val tags: List<String>,
|
||||
@SerialName("rating") val rating: Int?,
|
||||
@SerialName("dateAdded") val dateAdded: LocalDate,
|
||||
@SerialName("dateUpdated") val dateUpdated: LocalDateTime,
|
||||
@SerialName("recipeYield") val recipeYield: String = "",
|
||||
@SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponse>,
|
||||
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponse>,
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network.response
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeSummaryResponse(
|
||||
@SerialName("id") val remoteId: Long,
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("slug") val slug: String,
|
||||
@SerialName("image") val image: String,
|
||||
@SerialName("description") val description: String = "",
|
||||
@SerialName("recipeCategory") val recipeCategories: List<String>,
|
||||
@SerialName("tags") val tags: List<String>,
|
||||
@SerialName("rating") val rating: Int?,
|
||||
@SerialName("dateAdded") val dateAdded: LocalDate,
|
||||
@SerialName("dateUpdated") val dateUpdated: LocalDateTime
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "GetRecipeSummaryResponse(remoteId=$remoteId, name='$name')"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user