From 21c68588b3cde0e7c668c2bc22c7e462b022e63b Mon Sep 17 00:00:00 2001 From: iamwhoiam0 Date: Sat, 5 Jul 2025 15:20:01 +0300 Subject: [PATCH] done --- app/build.gradle.kts | 3 + app/src/main/AndroidManifest.xml | 2 + .../ru/otus/cookbook/ui/CookbookFragment.kt | 22 +++++- .../cookbook/ui/DeleteRecipeDialogFragment.kt | 45 +++++++++++ .../cookbook/ui/RecipeCategoryViewHolder.kt | 7 ++ .../ru/otus/cookbook/ui/RecipeDiffCallback.kt | 21 +++++ .../ru/otus/cookbook/ui/RecipeFragment.kt | 59 +++++++++++++- .../otus/cookbook/ui/RecipeItemViewHolder.kt | 7 ++ .../ru/otus/cookbook/ui/RecipesAdapter.kt | 67 ++++++++++++++++ app/src/main/res/drawable/arrow_back.xml | 9 +++ app/src/main/res/drawable/background.xml | 9 +++ app/src/main/res/drawable/background_item.xml | 25 ++++++ app/src/main/res/drawable/close.xml | 9 +++ .../main/res/drawable/shape_image_view.xml | 6 ++ .../res/drawable/shape_image_view_recipe.xml | 5 ++ app/src/main/res/drawable/trash_icon.xml | 9 +++ app/src/main/res/layout/activity_main.xml | 22 +++--- app/src/main/res/layout/fragment_cookbook.xml | 27 ++++++- app/src/main/res/layout/fragment_recipe.xml | 76 ++++++++++++++++++- .../main/res/layout/vh_recipe_category.xml | 26 ++++++- app/src/main/res/layout/vh_recipe_item.xml | 73 +++++++++++++++++- app/src/main/res/navigation/nav_graph.xml | 41 ++++++++++ app/src/main/res/values/colors.xml | 7 ++ app/src/main/res/values/strings.xml | 7 ++ app/src/main/res/values/themes.xml | 7 ++ gradle/libs.versions.toml | 6 +- 26 files changed, 570 insertions(+), 27 deletions(-) create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialogFragment.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/RecipeCategoryViewHolder.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDiffCallback.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/RecipeItemViewHolder.kt create mode 100644 app/src/main/kotlin/ru/otus/cookbook/ui/RecipesAdapter.kt create mode 100644 app/src/main/res/drawable/arrow_back.xml create mode 100644 app/src/main/res/drawable/background.xml create mode 100644 app/src/main/res/drawable/background_item.xml create mode 100644 app/src/main/res/drawable/close.xml create mode 100644 app/src/main/res/drawable/shape_image_view.xml create mode 100644 app/src/main/res/drawable/shape_image_view_recipe.xml create mode 100644 app/src/main/res/drawable/trash_icon.xml create mode 100644 app/src/main/res/navigation/nav_graph.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f3207a6..3def694 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,6 +48,9 @@ dependencies { implementation(libs.material) implementation(libs.androidx.activity) implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.recyclerview) + implementation(libs.glide) + annotationProcessor(libs.compiler) testImplementation(libs.junit) testImplementation(libs.kotlin.coroutines.test) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 063f4d1..431aadd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + (this) private val model: CookbookFragmentViewModel by viewModels { CookbookFragmentViewModel.Factory } + private val adapter = RecipesAdapter { launchRecipeFragment(it) } override fun onCreateView( inflater: LayoutInflater, @@ -28,6 +32,7 @@ class CookbookFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setupAppBar() setupRecyclerView() viewLifecycleOwner.lifecycleScope.launch { model.recipeList @@ -36,11 +41,24 @@ class CookbookFragment : Fragment() { } } + private fun setupAppBar() { + binding.withBinding { + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + (activity as? AppCompatActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(true) + toolbar.setNavigationOnClickListener { + findNavController().navigateUp() + } + } + } + private fun launchRecipeFragment(recipeId: Int) { + findNavController().navigate(CookbookFragmentDirections.actionCookbookFragmentToRecipeFragment(recipeId)) + } + private fun setupRecyclerView() = binding.withBinding { - // Setup RecyclerView + recyclerViewCategory.adapter = adapter } private fun onRecipeListUpdated(recipeList: List) { - // Handle recipe list + adapter.submitList(recipeList) } } \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialogFragment.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialogFragment.kt new file mode 100644 index 0000000..f137f83 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialogFragment.kt @@ -0,0 +1,45 @@ +package ru.otus.cookbook.ui + +import android.app.AlertDialog +import android.app.Dialog +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.dialog.MaterialDialogs +import ru.otus.cookbook.R +import kotlin.getValue + +class DeleteRecipeDialogFragment: DialogFragment() { + + private val args by navArgs() + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val recipeTitle = args.recipeTitle + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.delete)) + .setMessage(getString(R.string.are_you_sure_want_to_delete, recipeTitle)) + .setPositiveButton(getString(R.string.ok)) { _, _ -> + dismiss() + setResult(true) + } + .setNegativeButton(getString(R.string.cancel)) { _, _ -> + dismiss() + setResult(false) + } + .create() + } + + private fun setResult(result: Boolean) { + val navController = findNavController() + navController.previousBackStackEntry?.savedStateHandle?.set( + DIALOG_RESULT_KEY, + result + ) + } + + companion object { + const val DIALOG_RESULT_KEY = "delete" + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeCategoryViewHolder.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeCategoryViewHolder.kt new file mode 100644 index 0000000..7ff9269 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeCategoryViewHolder.kt @@ -0,0 +1,7 @@ +package ru.otus.cookbook.ui + +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import ru.otus.cookbook.databinding.VhRecipeCategoryBinding + +class RecipeCategoryViewHolder(val binding: VhRecipeCategoryBinding): ViewHolder(binding.root) { +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDiffCallback.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDiffCallback.kt new file mode 100644 index 0000000..f280ec2 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeDiffCallback.kt @@ -0,0 +1,21 @@ +package ru.otus.cookbook.ui + +import androidx.recyclerview.widget.DiffUtil +import ru.otus.cookbook.data.RecipeListItem + +class RecipeDiffCallback: DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean { + return when{ + oldItem is RecipeListItem.CategoryItem && newItem is RecipeListItem.CategoryItem -> + oldItem.name == newItem.name + oldItem is RecipeListItem.RecipeItem && newItem is RecipeListItem.RecipeItem -> + oldItem.id == newItem.id + else -> false + } + } + + override fun areContentsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean { + return oldItem == newItem + } + +} diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt index e4460c1..c699895 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt @@ -1,21 +1,29 @@ package ru.otus.cookbook.ui import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.MutableCreationExtras +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.bumptech.glide.Glide import kotlinx.coroutines.launch import ru.otus.cookbook.data.Recipe import ru.otus.cookbook.databinding.FragmentRecipeBinding class RecipeFragment : Fragment() { - private val recipeId: Int get() = TODO("Use Safe Args to get the recipe ID: https://developer.android.com/guide/navigation/use-graph/pass-data#Safe-args") + private val args by navArgs() + private val recipeId: Int get() = args.recipeId private val binding = FragmentBindingDelegate(this) private val model: RecipeFragmentViewModel by viewModels( @@ -38,6 +46,14 @@ class RecipeFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + binding.withBinding { + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + (activity as? AppCompatActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + setupAppBar() + observeDeleteResult() + viewLifecycleOwner.lifecycleScope.launch { model.recipe .flowWithLifecycle(viewLifecycleOwner.lifecycle) @@ -45,6 +61,21 @@ class RecipeFragment : Fragment() { } } + private fun observeDeleteResult() { + val navController = findNavController() + navController.currentBackStackEntry + ?.savedStateHandle + ?.getLiveData(DeleteRecipeDialogFragment.DIALOG_RESULT_KEY) + ?.observe(viewLifecycleOwner) { result -> + if (result) { + deleteRecipe() + Handler(Looper.getMainLooper()).post { + navController.navigateUp() + } + } + } + } + /** * Use to get recipe title and pass to confirmation dialog */ @@ -54,8 +85,32 @@ class RecipeFragment : Fragment() { private fun displayRecipe(recipe: Recipe) { // Display the recipe + binding.withBinding { + textViewTitle.text = recipe.title + textViewDescription.text = recipe.description + textViewRecipe.text = recipe.steps.mapIndexed { index, step -> "${index + 1}. $step"} + .joinToString("\n") + Glide.with(root) + .load(recipe.imageUrl) + .centerCrop() + .into(imageViewRecipe) + } + } + private fun setupAppBar() { + binding.withBinding { + (activity as? AppCompatActivity)?.supportActionBar?.title = getTitle() + toolbar.setNavigationOnClickListener { + findNavController().navigateUp() + } + imageViewDelete.setOnClickListener { + findNavController().navigate( + RecipeFragmentDirections.actionRecipeFragmentToDeleteRecipeDialogFragment( + getTitle() + ) + ) + } + } } - private fun deleteRecipe() { model.delete() } diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeItemViewHolder.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeItemViewHolder.kt new file mode 100644 index 0000000..3b71edf --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeItemViewHolder.kt @@ -0,0 +1,7 @@ +package ru.otus.cookbook.ui + +import androidx.recyclerview.widget.RecyclerView +import ru.otus.cookbook.databinding.VhRecipeItemBinding + +class RecipeItemViewHolder(val binding: VhRecipeItemBinding): RecyclerView.ViewHolder(binding.root) { +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipesAdapter.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipesAdapter.kt new file mode 100644 index 0000000..ee17236 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipesAdapter.kt @@ -0,0 +1,67 @@ +package ru.otus.cookbook.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.RequestOptions +import ru.otus.cookbook.data.RecipeListItem +import ru.otus.cookbook.databinding.VhRecipeCategoryBinding +import ru.otus.cookbook.databinding.VhRecipeItemBinding + +class RecipesAdapter(val onRecipeClick: (Int) -> Unit):ListAdapter(RecipeDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + + return when(viewType){ + VIEW_TYPE_CATEGORY -> { + val binding = VhRecipeCategoryBinding.inflate(inflater, parent, false) + RecipeCategoryViewHolder(binding) + } + VIEW_TYPE_RECIPE -> { + val binding = VhRecipeItemBinding.inflate(inflater, parent, false) + RecipeItemViewHolder(binding) + } + else -> throw RuntimeException("RuntimeException") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = getItem(position) + when(holder){ + is RecipeCategoryViewHolder -> { + val category = item as RecipeListItem.CategoryItem + holder.binding.textViewCategory.text = category.category.name + } + is RecipeItemViewHolder -> { + val recipeItem = item as RecipeListItem.RecipeItem + with(holder){ + binding.textBackground.text = recipeItem.title.first().toString() + binding.title.text = recipeItem.title + binding.description.text = recipeItem.description + Glide.with(binding.root.context) + .load(recipeItem.imageUrl) + .centerCrop() + .into(binding.imageViewRecipe) + binding.root.setOnClickListener { onRecipeClick(item.id) } + } + } + } + } + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is RecipeListItem.CategoryItem -> VIEW_TYPE_CATEGORY + is RecipeListItem.RecipeItem -> VIEW_TYPE_RECIPE + } + } + + companion object{ + private const val VIEW_TYPE_CATEGORY = 1 + private const val VIEW_TYPE_RECIPE = 2 + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_back.xml b/app/src/main/res/drawable/arrow_back.xml new file mode 100644 index 0000000..9241934 --- /dev/null +++ b/app/src/main/res/drawable/arrow_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/background.xml b/app/src/main/res/drawable/background.xml new file mode 100644 index 0000000..dbe7fd6 --- /dev/null +++ b/app/src/main/res/drawable/background.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/background_item.xml b/app/src/main/res/drawable/background_item.xml new file mode 100644 index 0000000..e391d08 --- /dev/null +++ b/app/src/main/res/drawable/background_item.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/close.xml b/app/src/main/res/drawable/close.xml new file mode 100644 index 0000000..84f6e03 --- /dev/null +++ b/app/src/main/res/drawable/close.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/shape_image_view.xml b/app/src/main/res/drawable/shape_image_view.xml new file mode 100644 index 0000000..57581db --- /dev/null +++ b/app/src/main/res/drawable/shape_image_view.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_image_view_recipe.xml b/app/src/main/res/drawable/shape_image_view_recipe.xml new file mode 100644 index 0000000..a01f3a8 --- /dev/null +++ b/app/src/main/res/drawable/shape_image_view_recipe.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/trash_icon.xml b/app/src/main/res/drawable/trash_icon.xml new file mode 100644 index 0000000..a74d7a1 --- /dev/null +++ b/app/src/main/res/drawable/trash_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 86a5d97..e0f0333 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,17 @@ - - - - - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_cookbook.xml b/app/src/main/res/layout/fragment_cookbook.xml index 77d9ef6..f729f7c 100644 --- a/app/src/main/res/layout/fragment_cookbook.xml +++ b/app/src/main/res/layout/fragment_cookbook.xml @@ -1,6 +1,27 @@ - + android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools" + android:orientation="vertical" + xmlns:app="http://schemas.android.com/apk/res-auto"> - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_recipe.xml b/app/src/main/res/layout/fragment_recipe.xml index 77d9ef6..5226005 100644 --- a/app/src/main/res/layout/fragment_recipe.xml +++ b/app/src/main/res/layout/fragment_recipe.xml @@ -1,6 +1,76 @@ - + android:layout_height="wrap_content" + android:fillViewport="true" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> - \ No newline at end of file + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/vh_recipe_category.xml b/app/src/main/res/layout/vh_recipe_category.xml index 006fd49..4222265 100644 --- a/app/src/main/res/layout/vh_recipe_category.xml +++ b/app/src/main/res/layout/vh_recipe_category.xml @@ -1,6 +1,28 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/vh_recipe_item.xml b/app/src/main/res/layout/vh_recipe_item.xml index 006fd49..4d1e128 100644 --- a/app/src/main/res/layout/vh_recipe_item.xml +++ b/app/src/main/res/layout/vh_recipe_item.xml @@ -1,6 +1,75 @@ + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:cardCornerRadius="10dp" + android:background="@drawable/background_item" + android:layout_marginTop="12dp" + android:layout_marginHorizontal="16dp" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 0000000..fb928e2 --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c8524cd..4b988ba 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,4 +2,11 @@ #FF000000 #FFFFFFFF + #65558F + #EADDFF + #4F378A + #CAC4D0 + #625B71 + #7D5260 + #EFDAE5 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fce1d1..441c38f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,10 @@ Cookbook + + Hello blank fragment + + Delete + Are you sure want to delete %1$s? + ОК + Cancel \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 47d6575..427c7eb 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -5,5 +5,12 @@ + +