Replace "Mealie" with "Mealient" everywhere
This commit is contained in:
8
app/src/main/java/gq/kirmanak/mealient/ui/ImageLoader.kt
Normal file
8
app/src/main/java/gq/kirmanak/mealient/ui/ImageLoader.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package gq.kirmanak.mealient.ui
|
||||
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.DrawableRes
|
||||
|
||||
interface ImageLoader {
|
||||
fun loadImage(url: String?, @DrawableRes placeholderId: Int, imageView: ImageView)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package gq.kirmanak.mealient.ui
|
||||
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.onFailure
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import timber.log.Timber
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
object SwipeRefreshLayoutHelper {
|
||||
private fun SwipeRefreshLayout.refreshesFlow(): Flow<Unit> {
|
||||
Timber.v("refreshesFlow() called")
|
||||
return callbackFlow {
|
||||
val listener = SwipeRefreshLayout.OnRefreshListener {
|
||||
Timber.v("Refresh requested")
|
||||
trySendBlocking(Unit).onFailure { Timber.e(it, "Can't send refresh callback") }
|
||||
}
|
||||
Timber.v("Adding refresh request listener")
|
||||
setOnRefreshListener(listener)
|
||||
awaitClose {
|
||||
Timber.v("Removing refresh request listener")
|
||||
setOnRefreshListener(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T : Any, VH : RecyclerView.ViewHolder> PagingDataAdapter<T, VH>.listenToRefreshRequests(
|
||||
refreshLayout: SwipeRefreshLayout
|
||||
) {
|
||||
Timber.v("listenToRefreshRequests() called")
|
||||
refreshLayout.refreshesFlow().collectLatest {
|
||||
Timber.d("Received refresh request")
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
14
app/src/main/java/gq/kirmanak/mealient/ui/UiModule.kt
Normal file
14
app/src/main/java/gq/kirmanak/mealient/ui/UiModule.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package gq.kirmanak.mealient.ui
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import gq.kirmanak.mealient.ui.glide.ImageLoaderGlide
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface UiModule {
|
||||
@Binds
|
||||
fun bindImageLoader(imageLoaderGlide: ImageLoaderGlide): ImageLoader
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package gq.kirmanak.mealient.ui.auth
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AuthenticationFragment : Fragment() {
|
||||
private var _binding: FragmentAuthenticationBinding? = null
|
||||
private val binding: FragmentAuthenticationBinding
|
||||
get() = checkNotNull(_binding) { "Binding requested when fragment is off screen" }
|
||||
private val viewModel by viewModels<AuthenticationViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState")
|
||||
listenToAuthenticationStatuses()
|
||||
}
|
||||
|
||||
private fun listenToAuthenticationStatuses() {
|
||||
Timber.d("listenToAuthenticationStatuses() called")
|
||||
lifecycleScope.launchWhenCreated {
|
||||
viewModel.authenticationStatuses().collectLatest {
|
||||
Timber.d("listenToAuthenticationStatuses: new status = $it")
|
||||
if (it) navigateToRecipes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
Timber.v("onCreateView() called with: inflater = $inflater, container = $container, savedInstanceState = $savedInstanceState")
|
||||
_binding = FragmentAuthenticationBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
||||
binding.button.setOnClickListener { onLoginClicked() }
|
||||
}
|
||||
|
||||
private fun navigateToRecipes() {
|
||||
Timber.v("navigateToRecipes() called")
|
||||
findNavController().navigate(AuthenticationFragmentDirections.actionAuthenticationFragmentToRecipesFragment())
|
||||
}
|
||||
|
||||
private fun onLoginClicked() {
|
||||
Timber.v("onLoginClicked() called")
|
||||
val email: String
|
||||
val pass: String
|
||||
val url: String
|
||||
with(binding) {
|
||||
email = checkIfInputIsEmpty(emailInput, emailInputLayout) {
|
||||
"Email is empty"
|
||||
} ?: return
|
||||
pass = checkIfInputIsEmpty(passwordInput, passwordInputLayout) {
|
||||
"Pass is empty"
|
||||
} ?: return
|
||||
url = checkIfInputIsEmpty(urlInput, urlInputLayout) {
|
||||
"URL is empty"
|
||||
} ?: return
|
||||
}
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
|
||||
runCatching {
|
||||
viewModel.authenticate(email, pass, url)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Can't authenticate")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkIfInputIsEmpty(
|
||||
input: EditText,
|
||||
inputLayout: TextInputLayout,
|
||||
errorText: () -> String
|
||||
): String? {
|
||||
Timber.v("checkIfInputIsEmpty() called with: input = $input, inputLayout = $inputLayout, errorText = $errorText")
|
||||
val text = input.text?.toString()
|
||||
Timber.d("Input text is \"$text\"")
|
||||
if (text.isNullOrBlank()) {
|
||||
inputLayout.error = errorText()
|
||||
return null
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
Timber.v("onDestroyView() called")
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package gq.kirmanak.mealient.ui.auth
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AuthenticationViewModel @Inject constructor(
|
||||
private val authRepo: AuthRepo,
|
||||
private val recipeRepo: RecipeRepo
|
||||
) : ViewModel() {
|
||||
init {
|
||||
Timber.v("constructor called")
|
||||
}
|
||||
|
||||
suspend fun authenticate(username: String, password: String, baseUrl: String) {
|
||||
Timber.v("authenticate() called with: username = $username, password = $password, baseUrl = $baseUrl")
|
||||
authRepo.authenticate(username, password, baseUrl)
|
||||
}
|
||||
|
||||
fun authenticationStatuses(): Flow<Boolean> {
|
||||
Timber.v("authenticationStatuses() called")
|
||||
return authRepo.authenticationStatuses()
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
Timber.v("logout() called")
|
||||
authRepo.logout()
|
||||
viewModelScope.launch { recipeRepo.clearLocalData() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package gq.kirmanak.mealient.ui.glide
|
||||
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.DrawableRes
|
||||
import gq.kirmanak.mealient.ui.ImageLoader
|
||||
import javax.inject.Inject
|
||||
|
||||
class ImageLoaderGlide @Inject constructor() : ImageLoader {
|
||||
override fun loadImage(url: String?, @DrawableRes placeholderId: Int, imageView: ImageView) {
|
||||
GlideApp.with(imageView)
|
||||
.load(url)
|
||||
.centerCrop()
|
||||
.placeholder(placeholderId)
|
||||
.into(imageView)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package gq.kirmanak.mealient.ui.glide
|
||||
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
|
||||
@GlideModule
|
||||
class MainGlideModule : AppGlideModule()
|
||||
@@ -0,0 +1,22 @@
|
||||
package gq.kirmanak.mealient.ui.recipes
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
|
||||
|
||||
class RecipeViewHolder(
|
||||
private val binding: ViewHolderRecipeBinding,
|
||||
private val recipeViewModel: RecipeViewModel,
|
||||
private val clickListener: (RecipeSummaryEntity) -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
private val loadingPlaceholder by lazy {
|
||||
binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder)
|
||||
}
|
||||
|
||||
fun bind(item: RecipeSummaryEntity?) {
|
||||
binding.name.text = item?.name ?: loadingPlaceholder
|
||||
recipeViewModel.loadRecipeImage(binding.image, item)
|
||||
item?.let { entity -> binding.root.setOnClickListener { clickListener(entity) } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package gq.kirmanak.mealient.ui.recipes
|
||||
|
||||
import android.widget.ImageView
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeImageLoader
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class RecipeViewModel @Inject constructor(
|
||||
recipeRepo: RecipeRepo,
|
||||
private val recipeImageLoader: RecipeImageLoader
|
||||
) : ViewModel() {
|
||||
val recipeFlow = recipeRepo.createPager().flow
|
||||
|
||||
fun loadRecipeImage(view: ImageView, recipeSummary: RecipeSummaryEntity?) {
|
||||
viewModelScope.launch {
|
||||
recipeImageLoader.loadRecipeImage(view, recipeSummary?.slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package gq.kirmanak.mealient.ui.recipes
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.databinding.FragmentRecipesBinding
|
||||
import gq.kirmanak.mealient.ui.SwipeRefreshLayoutHelper.listenToRefreshRequests
|
||||
import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import timber.log.Timber
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@AndroidEntryPoint
|
||||
class RecipesFragment : Fragment() {
|
||||
private var _binding: FragmentRecipesBinding? = null
|
||||
private val binding: FragmentRecipesBinding
|
||||
get() = checkNotNull(_binding) { "Binding requested when fragment is off screen" }
|
||||
private val viewModel by viewModels<RecipeViewModel>()
|
||||
private val authViewModel by viewModels<AuthenticationViewModel>()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
Timber.v("onCreateView() called with: inflater = $inflater, container = $container, savedInstanceState = $savedInstanceState")
|
||||
_binding = FragmentRecipesBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
||||
setupRecipeAdapter()
|
||||
listenToAuthStatuses()
|
||||
}
|
||||
|
||||
private fun navigateToRecipeInfo(recipeSummaryEntity: RecipeSummaryEntity) {
|
||||
findNavController().navigate(
|
||||
RecipesFragmentDirections.actionRecipesFragmentToRecipeInfoFragment(
|
||||
recipeSlug = recipeSummaryEntity.slug,
|
||||
recipeId = recipeSummaryEntity.remoteId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun listenToAuthStatuses() {
|
||||
Timber.v("listenToAuthStatuses() called")
|
||||
lifecycleScope.launchWhenCreated {
|
||||
authViewModel.authenticationStatuses().collectLatest {
|
||||
Timber.v("listenToAuthStatuses: new auth status = $it")
|
||||
if (!it) navigateToAuthFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToAuthFragment() {
|
||||
Timber.v("navigateToAuthFragment() called")
|
||||
findNavController().navigate(RecipesFragmentDirections.actionRecipesFragmentToAuthenticationFragment())
|
||||
}
|
||||
|
||||
private fun setupRecipeAdapter() {
|
||||
Timber.v("setupRecipeAdapter() called")
|
||||
binding.recipes.layoutManager = LinearLayoutManager(requireContext())
|
||||
val adapter = RecipesPagingAdapter(viewModel) { navigateToRecipeInfo(it) }
|
||||
binding.recipes.adapter = adapter
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
|
||||
viewModel.recipeFlow.collect {
|
||||
Timber.d("Received update")
|
||||
adapter.submitData(it)
|
||||
}
|
||||
}
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
|
||||
adapter.listenToRefreshRequests(binding.refresher)
|
||||
}
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
|
||||
adapter.onPagesUpdatedFlow.collect {
|
||||
Timber.d("Pages have been updated")
|
||||
binding.refresher.isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
Timber.v("onDestroyView() called")
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package gq.kirmanak.mealient.ui.recipes
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
|
||||
import timber.log.Timber
|
||||
|
||||
class RecipesPagingAdapter(
|
||||
private val viewModel: RecipeViewModel,
|
||||
private val clickListener: (RecipeSummaryEntity) -> Unit
|
||||
) : PagingDataAdapter<RecipeSummaryEntity, RecipeViewHolder>(RecipeDiffCallback) {
|
||||
override fun onBindViewHolder(holder: RecipeViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
holder.bind(item)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeViewHolder {
|
||||
Timber.v("onCreateViewHolder() called with: parent = $parent, viewType = $viewType")
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val binding = ViewHolderRecipeBinding.inflate(inflater, parent, false)
|
||||
return RecipeViewHolder(binding, viewModel, clickListener)
|
||||
}
|
||||
|
||||
private object RecipeDiffCallback : DiffUtil.ItemCallback<RecipeSummaryEntity>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: RecipeSummaryEntity,
|
||||
newItem: RecipeSummaryEntity
|
||||
): Boolean {
|
||||
return oldItem.remoteId == newItem.remoteId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: RecipeSummaryEntity,
|
||||
newItem: RecipeSummaryEntity
|
||||
): Boolean {
|
||||
return oldItem.name == newItem.name && oldItem.slug == newItem.slug
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.info
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.databinding.FragmentRecipeInfoBinding
|
||||
import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RecipeInfoFragment : Fragment() {
|
||||
private var _binding: FragmentRecipeInfoBinding? = null
|
||||
private val binding: FragmentRecipeInfoBinding
|
||||
get() = checkNotNull(_binding) { "Binding requested when fragment is off screen" }
|
||||
private val authViewModel by viewModels<AuthenticationViewModel>()
|
||||
private val arguments by navArgs<RecipeInfoFragmentArgs>()
|
||||
private val viewModel by viewModels<RecipeInfoViewModel>()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
Timber.v("onCreateView() called with: inflater = $inflater, container = $container, savedInstanceState = $savedInstanceState")
|
||||
_binding = FragmentRecipeInfoBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
||||
listenToAuthStatuses()
|
||||
viewModel.loadRecipeImage(binding.image, arguments.recipeSlug)
|
||||
viewModel.loadRecipeInfo(arguments.recipeId, arguments.recipeSlug)
|
||||
viewModel.recipeInfo.observe(viewLifecycleOwner) {
|
||||
binding.title.text = it.recipeSummaryEntity.name
|
||||
binding.description.text = it.recipeSummaryEntity.description
|
||||
|
||||
val recipeIngredientsAdapter = RecipeIngredientsAdapter()
|
||||
binding.ingredientsList.adapter = recipeIngredientsAdapter
|
||||
binding.ingredientsList.layoutManager = LinearLayoutManager(requireContext())
|
||||
recipeIngredientsAdapter.submitList(it.recipeIngredients)
|
||||
|
||||
val recipeInstructionsAdapter = RecipeInstructionsAdapter()
|
||||
binding.instructionsList.adapter = recipeInstructionsAdapter
|
||||
binding.instructionsList.layoutManager = LinearLayoutManager(requireContext())
|
||||
recipeInstructionsAdapter.submitList(it.recipeInstructions)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenToAuthStatuses() {
|
||||
Timber.v("listenToAuthStatuses() called")
|
||||
lifecycleScope.launchWhenCreated {
|
||||
authViewModel.authenticationStatuses().collectLatest {
|
||||
Timber.v("listenToAuthStatuses: new auth status = $it")
|
||||
if (!it) navigateToAuthFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToAuthFragment() {
|
||||
Timber.v("navigateToAuthFragment() called")
|
||||
findNavController().navigate(RecipeInfoFragmentDirections.actionRecipeInfoFragmentToAuthenticationFragment())
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
Timber.v("onDestroyView() called")
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.info
|
||||
|
||||
import android.widget.ImageView
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeImageLoader
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class RecipeInfoViewModel @Inject constructor(
|
||||
private val recipeRepo: RecipeRepo,
|
||||
private val recipeImageLoader: RecipeImageLoader
|
||||
) : ViewModel() {
|
||||
private val _recipeInfo = MutableLiveData<FullRecipeInfo>()
|
||||
val recipeInfo: LiveData<FullRecipeInfo> = _recipeInfo
|
||||
|
||||
fun loadRecipeImage(view: ImageView, recipeSlug: String) {
|
||||
viewModelScope.launch {
|
||||
recipeImageLoader.loadRecipeImage(view, recipeSlug)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadRecipeInfo(recipeId: Long, recipeSlug: String) {
|
||||
Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug")
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
recipeRepo.loadRecipeInfo(recipeId, recipeSlug)
|
||||
}.onSuccess {
|
||||
Timber.d("loadRecipeInfo: received recipe info = $it")
|
||||
_recipeInfo.value = it
|
||||
}.onFailure {
|
||||
Timber.e(it, "loadRecipeInfo: can't load recipe info")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.info
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeIngredientEntity
|
||||
import gq.kirmanak.mealient.databinding.ViewHolderIngredientBinding
|
||||
import gq.kirmanak.mealient.ui.recipes.info.RecipeIngredientsAdapter.RecipeIngredientViewHolder
|
||||
|
||||
class RecipeIngredientsAdapter() :
|
||||
ListAdapter<RecipeIngredientEntity, RecipeIngredientViewHolder>(RecipeIngredientDiffCallback) {
|
||||
|
||||
class RecipeIngredientViewHolder(
|
||||
private val binding: ViewHolderIngredientBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: RecipeIngredientEntity) {
|
||||
binding.checkBox.text = item.note
|
||||
}
|
||||
}
|
||||
|
||||
private object RecipeIngredientDiffCallback : DiffUtil.ItemCallback<RecipeIngredientEntity>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: RecipeIngredientEntity,
|
||||
newItem: RecipeIngredientEntity
|
||||
): Boolean = oldItem.localId == newItem.localId
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: RecipeIngredientEntity,
|
||||
newItem: RecipeIngredientEntity
|
||||
): Boolean = oldItem == newItem
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeIngredientViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return RecipeIngredientViewHolder(
|
||||
ViewHolderIngredientBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecipeIngredientViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.info
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeInstructionEntity
|
||||
import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding
|
||||
|
||||
class RecipeInstructionsAdapter :
|
||||
ListAdapter<RecipeInstructionEntity, RecipeInstructionsAdapter.RecipeInstructionViewHolder>(
|
||||
RecipeInstructionDiffCallback
|
||||
) {
|
||||
|
||||
private object RecipeInstructionDiffCallback :
|
||||
DiffUtil.ItemCallback<RecipeInstructionEntity>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: RecipeInstructionEntity,
|
||||
newItem: RecipeInstructionEntity
|
||||
): Boolean = oldItem.localId == newItem.localId
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: RecipeInstructionEntity,
|
||||
newItem: RecipeInstructionEntity
|
||||
): Boolean = oldItem == newItem
|
||||
}
|
||||
|
||||
class RecipeInstructionViewHolder(private val binding: ViewHolderInstructionBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: RecipeInstructionEntity, position: Int) {
|
||||
binding.step.text = "Step: ${position + 1}"
|
||||
binding.instruction.text = item.text
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeInstructionViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return RecipeInstructionViewHolder(
|
||||
ViewHolderInstructionBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecipeInstructionViewHolder, position: Int) {
|
||||
holder.bind(getItem(position), position)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user