diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt index 92758cc..1e4d118 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt @@ -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) + } + } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt index b7c1266..c1e3074 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt @@ -17,4 +17,8 @@ interface RecipeRepo { fun updateNameQuery(name: String?) suspend fun refreshRecipes() + + suspend fun removeFavoriteRecipe(recipeSlug: String) + + suspend fun addFavoriteRecipe(recipeSlug: String) } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt index 80840bc..62184eb 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt @@ -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 diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt index db6f42c..40578aa 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt @@ -6,4 +6,8 @@ interface RecipeDataSource { suspend fun requestRecipeInfo(slug: String): FullRecipeInfo suspend fun getFavoriteRecipes(): List + + suspend fun removeFavoriteRecipe(recipeSlug: String) + + suspend fun addFavoriteRecipe(recipeSlug: String) } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt index 41f4dc4..cd263b7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt @@ -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) { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListFragment.kt index 687f258..0767e60 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListFragment.kt @@ -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 diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModel.kt index 2536f8a..9e3ec9f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModel.kt @@ -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) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesPagingAdapter.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesPagingAdapter.kt index cde6fb9..e36621a 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesPagingAdapter.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesPagingAdapter.kt @@ -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(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() { 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 } } diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt index a1724d1..2358c64 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt @@ -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) } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt index 4c0ea0b..146b9af 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt @@ -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" } + ) } diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt index 5c139d3..c3359a4 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt @@ -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 + ) } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/GetUserInfoResponseV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/GetUserInfoResponseV0.kt index 6110e2e..005ccf1 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/GetUserInfoResponseV0.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/GetUserInfoResponseV0.kt @@ -5,5 +5,6 @@ import kotlinx.serialization.Serializable @Serializable data class GetUserInfoResponseV0( + @SerialName("id") val id: Int, @SerialName("favoriteRecipes") val favoriteRecipes: List = emptyList(), ) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt index d9b6a57..482974f 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt @@ -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) } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt index be92d74..6c0b094 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt @@ -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" } + ) } diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt index 5e9e6f5..d1dd7cb 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt @@ -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 + ) } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetUserInfoResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetUserInfoResponseV1.kt index 4fda4f4..ee97e60 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetUserInfoResponseV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetUserInfoResponseV1.kt @@ -5,5 +5,6 @@ import kotlinx.serialization.Serializable @Serializable data class GetUserInfoResponseV1( + @SerialName("id") val id: String, @SerialName("favoriteRecipes") val favoriteRecipes: List = emptyList(), )