Update favorite status on icon click
This commit is contained in:
@@ -68,4 +68,26 @@ class MealieDataSourceWrapper @Inject constructor(
|
||||
ServerVersion.V0 -> v0Source.requestUserInfo().favoriteRecipes
|
||||
ServerVersion.V1 -> v1Source.requestUserInfo().favoriteRecipes
|
||||
}
|
||||
|
||||
override suspend fun removeFavoriteRecipe(recipeSlug: String) = when (getVersion()) {
|
||||
ServerVersion.V0 -> {
|
||||
val userId = v0Source.requestUserInfo().id
|
||||
v0Source.removeFavoriteRecipe(userId, recipeSlug)
|
||||
}
|
||||
ServerVersion.V1 -> {
|
||||
val userId = v1Source.requestUserInfo().id
|
||||
v1Source.removeFavoriteRecipe(userId, recipeSlug)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun addFavoriteRecipe(recipeSlug: String) = when (getVersion()) {
|
||||
ServerVersion.V0 -> {
|
||||
val userId = v0Source.requestUserInfo().id
|
||||
v0Source.addFavoriteRecipe(userId, recipeSlug)
|
||||
}
|
||||
ServerVersion.V1 -> {
|
||||
val userId = v1Source.requestUserInfo().id
|
||||
v1Source.addFavoriteRecipe(userId, recipeSlug)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,4 +17,8 @@ interface RecipeRepo {
|
||||
fun updateNameQuery(name: String?)
|
||||
|
||||
suspend fun refreshRecipes()
|
||||
|
||||
suspend fun removeFavoriteRecipe(recipeSlug: String)
|
||||
|
||||
suspend fun addFavoriteRecipe(recipeSlug: String)
|
||||
}
|
||||
@@ -72,6 +72,24 @@ class RecipeRepoImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeFavoriteRecipe(recipeSlug: String) {
|
||||
logger.v { "removeFavoriteRecipe() called with: recipeSlug = $recipeSlug" }
|
||||
runCatchingExceptCancel {
|
||||
dataSource.removeFavoriteRecipe(recipeSlug)
|
||||
}.onFailure {
|
||||
logger.e(it) { "Can't remove a favorite recipe" }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun addFavoriteRecipe(recipeSlug: String) {
|
||||
logger.v { "addFavoriteRecipe() called with: recipeSlug = $recipeSlug" }
|
||||
runCatchingExceptCancel {
|
||||
dataSource.addFavoriteRecipe(recipeSlug)
|
||||
}.onFailure {
|
||||
logger.e(it) { "Can't add a favorite recipe" }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LOAD_PAGE_SIZE = 50
|
||||
private const val INITIAL_LOAD_PAGE_SIZE = LOAD_PAGE_SIZE * 3
|
||||
|
||||
@@ -6,4 +6,8 @@ interface RecipeDataSource {
|
||||
suspend fun requestRecipeInfo(slug: String): FullRecipeInfo
|
||||
|
||||
suspend fun getFavoriteRecipes(): List<String>
|
||||
|
||||
suspend fun removeFavoriteRecipe(recipeSlug: String)
|
||||
|
||||
suspend fun addFavoriteRecipe(recipeSlug: String)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package gq.kirmanak.mealient.ui.recipes
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.hilt.android.scopes.FragmentScoped
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
|
||||
@@ -10,28 +11,41 @@ import gq.kirmanak.mealient.extensions.resources
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
class RecipeViewHolder private constructor(
|
||||
private val logger: Logger,
|
||||
private val binding: ViewHolderRecipeBinding,
|
||||
private val recipeImageLoader: RecipeImageLoader,
|
||||
private val clickListener: (RecipeSummaryEntity) -> Unit,
|
||||
private val clickListener: (ClickEvent) -> Unit,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
@Singleton
|
||||
@FragmentScoped
|
||||
class Factory @Inject constructor(
|
||||
private val recipeImageLoader: RecipeImageLoader,
|
||||
private val logger: Logger,
|
||||
) {
|
||||
|
||||
fun build(
|
||||
recipeImageLoader: RecipeImageLoader,
|
||||
binding: ViewHolderRecipeBinding,
|
||||
clickListener: (RecipeSummaryEntity) -> Unit,
|
||||
clickListener: (ClickEvent) -> Unit,
|
||||
) = RecipeViewHolder(logger, binding, recipeImageLoader, clickListener)
|
||||
|
||||
}
|
||||
|
||||
sealed class ClickEvent {
|
||||
|
||||
abstract val recipeSummaryEntity: RecipeSummaryEntity
|
||||
|
||||
data class FavoriteClick(
|
||||
override val recipeSummaryEntity: RecipeSummaryEntity
|
||||
) : ClickEvent()
|
||||
|
||||
data class RecipeClick(
|
||||
override val recipeSummaryEntity: RecipeSummaryEntity
|
||||
) : ClickEvent()
|
||||
|
||||
}
|
||||
|
||||
private val loadingPlaceholder by lazy {
|
||||
binding.resources.getString(R.string.view_holder_recipe_text_placeholder)
|
||||
}
|
||||
@@ -43,7 +57,10 @@ class RecipeViewHolder private constructor(
|
||||
item?.let { entity ->
|
||||
binding.root.setOnClickListener {
|
||||
logger.d { "bind: item clicked $entity" }
|
||||
clickListener(entity)
|
||||
clickListener(ClickEvent.RecipeClick(entity))
|
||||
}
|
||||
binding.favoriteIcon.setOnClickListener {
|
||||
clickListener(ClickEvent.FavoriteClick(entity))
|
||||
}
|
||||
binding.favoriteIcon.setImageResource(
|
||||
if (item.isFavorite) {
|
||||
|
||||
@@ -90,7 +90,16 @@ class RecipesListFragment : Fragment(R.layout.fragment_recipes_list) {
|
||||
private fun setupRecipeAdapter() {
|
||||
logger.v { "setupRecipeAdapter() called" }
|
||||
|
||||
val recipesAdapter = recipePagingAdapterFactory.build { onRecipeClicked(it) }
|
||||
val recipesAdapter = recipePagingAdapterFactory.build {
|
||||
when (it) {
|
||||
is RecipeViewHolder.ClickEvent.FavoriteClick -> {
|
||||
viewModel.onFavoriteIconClick(it.recipeSummaryEntity)
|
||||
}
|
||||
is RecipeViewHolder.ClickEvent.RecipeClick -> {
|
||||
onRecipeClicked(it.recipeSummaryEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with(binding.recipes) {
|
||||
adapter = recipesAdapter
|
||||
|
||||
@@ -8,10 +8,12 @@ import androidx.paging.cachedIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.extensions.valueUpdatesOnly
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -38,4 +40,15 @@ class RecipesListViewModel @Inject constructor(
|
||||
emit(result)
|
||||
}
|
||||
}
|
||||
|
||||
fun onFavoriteIconClick(recipeSummaryEntity: RecipeSummaryEntity) {
|
||||
logger.v { "onFavoriteIconClick() called with: recipeSummaryEntity = $recipeSummaryEntity" }
|
||||
viewModelScope.launch {
|
||||
if (recipeSummaryEntity.isFavorite) {
|
||||
recipeRepo.removeFavoriteRecipe(recipeSummaryEntity.slug)
|
||||
} else {
|
||||
recipeRepo.addFavoriteRecipe(recipeSummaryEntity.slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,28 +8,22 @@ import dagger.hilt.android.scopes.FragmentScoped
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
||||
import javax.inject.Inject
|
||||
|
||||
class RecipesPagingAdapter private constructor(
|
||||
private val logger: Logger,
|
||||
private val recipeImageLoader: RecipeImageLoader,
|
||||
private val recipeViewHolderFactory: RecipeViewHolder.Factory,
|
||||
private val clickListener: (RecipeSummaryEntity) -> Unit
|
||||
private val clickListener: (RecipeViewHolder.ClickEvent) -> Unit
|
||||
) : PagingDataAdapter<RecipeSummaryEntity, RecipeViewHolder>(RecipeDiffCallback) {
|
||||
|
||||
@FragmentScoped
|
||||
class Factory @Inject constructor(
|
||||
private val logger: Logger,
|
||||
private val recipeViewHolderFactory: RecipeViewHolder.Factory,
|
||||
private val recipeImageLoader: RecipeImageLoader,
|
||||
) {
|
||||
|
||||
fun build(clickListener: (RecipeSummaryEntity) -> Unit) = RecipesPagingAdapter(
|
||||
logger,
|
||||
recipeImageLoader,
|
||||
recipeViewHolderFactory,
|
||||
clickListener
|
||||
fun build(clickListener: (RecipeViewHolder.ClickEvent) -> Unit) = RecipesPagingAdapter(
|
||||
logger, recipeViewHolderFactory, clickListener
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,18 +37,16 @@ class RecipesPagingAdapter private constructor(
|
||||
logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" }
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val binding = ViewHolderRecipeBinding.inflate(inflater, parent, false)
|
||||
return recipeViewHolderFactory.build(recipeImageLoader, binding, clickListener)
|
||||
return recipeViewHolderFactory.build(binding, clickListener)
|
||||
}
|
||||
|
||||
private object RecipeDiffCallback : DiffUtil.ItemCallback<RecipeSummaryEntity>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: RecipeSummaryEntity,
|
||||
newItem: RecipeSummaryEntity
|
||||
oldItem: RecipeSummaryEntity, newItem: RecipeSummaryEntity
|
||||
): Boolean = oldItem.remoteId == newItem.remoteId
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: RecipeSummaryEntity,
|
||||
newItem: RecipeSummaryEntity
|
||||
oldItem: RecipeSummaryEntity, newItem: RecipeSummaryEntity
|
||||
): Boolean = oldItem.name == newItem.name && oldItem.slug == newItem.slug
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,4 +42,8 @@ interface MealieDataSourceV0 {
|
||||
): String
|
||||
|
||||
suspend fun requestUserInfo(): GetUserInfoResponseV0
|
||||
|
||||
suspend fun removeFavoriteRecipe(userId: Int, recipeSlug: String)
|
||||
|
||||
suspend fun addFavoriteRecipe(userId: Int, recipeSlug: String)
|
||||
}
|
||||
@@ -97,4 +97,22 @@ class MealieDataSourceV0Impl @Inject constructor(
|
||||
logMethod = { "requestUserInfo" },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun removeFavoriteRecipe(
|
||||
userId: Int,
|
||||
recipeSlug: String
|
||||
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.removeFavoriteRecipe(userId, recipeSlug) },
|
||||
logMethod = { "removeFavoriteRecipe" },
|
||||
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
|
||||
)
|
||||
|
||||
override suspend fun addFavoriteRecipe(
|
||||
userId: Int,
|
||||
recipeSlug: String
|
||||
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.addFavoriteRecipe(userId, recipeSlug) },
|
||||
logMethod = { "addFavoriteRecipe" },
|
||||
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,4 +43,16 @@ interface MealieServiceV0 {
|
||||
|
||||
@GET("/api/users/self")
|
||||
suspend fun getUserSelfInfo(): GetUserInfoResponseV0
|
||||
|
||||
@DELETE("/api/users/{userId}/favorites/{recipeSlug}")
|
||||
suspend fun removeFavoriteRecipe(
|
||||
@Path("userId") userId: Int,
|
||||
@Path("recipeSlug") recipeSlug: String
|
||||
)
|
||||
|
||||
@POST("/api/users/{userId}/favorites/{recipeSlug}")
|
||||
suspend fun addFavoriteRecipe(
|
||||
@Path("userId") userId: Int,
|
||||
@Path("recipeSlug") recipeSlug: String
|
||||
)
|
||||
}
|
||||
@@ -5,5 +5,6 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetUserInfoResponseV0(
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("favoriteRecipes") val favoriteRecipes: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
@@ -50,4 +50,8 @@ interface MealieDataSourceV1 {
|
||||
): CreateApiTokenResponseV1
|
||||
|
||||
suspend fun requestUserInfo(): GetUserInfoResponseV1
|
||||
|
||||
suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String)
|
||||
|
||||
suspend fun addFavoriteRecipe(userId: String, recipeSlug: String)
|
||||
}
|
||||
@@ -108,5 +108,23 @@ class MealieDataSourceV1Impl @Inject constructor(
|
||||
logMethod = { "requestUserInfo" },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun removeFavoriteRecipe(
|
||||
userId: String,
|
||||
recipeSlug: String
|
||||
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.removeFavoriteRecipe(userId, recipeSlug) },
|
||||
logMethod = { "removeFavoriteRecipe" },
|
||||
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
|
||||
)
|
||||
|
||||
override suspend fun addFavoriteRecipe(
|
||||
userId: String,
|
||||
recipeSlug: String
|
||||
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.addFavoriteRecipe(userId, recipeSlug) },
|
||||
logMethod = { "addFavoriteRecipe" },
|
||||
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -49,4 +49,16 @@ interface MealieServiceV1 {
|
||||
|
||||
@GET("/api/users/self")
|
||||
suspend fun getUserSelfInfo(): GetUserInfoResponseV1
|
||||
|
||||
@DELETE("/api/users/{userId}/favorites/{recipeSlug}")
|
||||
suspend fun removeFavoriteRecipe(
|
||||
@Path("userId") userId: String,
|
||||
@Path("recipeSlug") recipeSlug: String
|
||||
)
|
||||
|
||||
@POST("/api/users/{userId}/favorites/{recipeSlug}")
|
||||
suspend fun addFavoriteRecipe(
|
||||
@Path("userId") userId: String,
|
||||
@Path("recipeSlug") recipeSlug: String
|
||||
)
|
||||
}
|
||||
@@ -5,5 +5,6 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetUserInfoResponseV1(
|
||||
@SerialName("id") val id: String,
|
||||
@SerialName("favoriteRecipes") val favoriteRecipes: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user