diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f3207a6..3a7909e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,4 +50,5 @@ dependencies { implementation(libs.androidx.constraintlayout) testImplementation(libs.junit) testImplementation(libs.kotlin.coroutines.test) + implementation("io.coil-kt:coil:2.4.0") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 063f4d1..682aa63 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 var adapter: RecipeAdapter? = null + override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = binding.bind( - container, - FragmentCookbookBinding::inflate + container, FragmentCookbookBinding::inflate ) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.withBinding { + btnClose.setOnClickListener { + requireActivity().finish() + } + } setupRecyclerView() viewLifecycleOwner.lifecycleScope.launch { - model.recipeList - .flowWithLifecycle(viewLifecycleOwner.lifecycle) + model.recipeList.flowWithLifecycle(viewLifecycleOwner.lifecycle) .collect(::onRecipeListUpdated) } } private fun setupRecyclerView() = binding.withBinding { - // Setup RecyclerView + adapter = RecipeAdapter { recipeId -> + val action = CookbookFragmentDirections.actionCookbookFragmentToRecipeFragment(recipeId) + findNavController().navigate(action) + } + recyclerView.layoutManager = LinearLayoutManager(context) + recyclerView.adapter = adapter } private fun onRecipeListUpdated(recipeList: List) { - // Handle recipe list + adapter?.items = recipeList + } + + override fun onDestroyView() { + super.onDestroyView() + adapter = null } } \ 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..0850a8f --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/DeleteRecipeDialogFragment.kt @@ -0,0 +1,23 @@ +package ru.otus.cookbook.ui + +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 ru.otus.cookbook.R + +class DeleteRecipeDialogFragment : DialogFragment() { + + private val args: DeleteRecipeDialogFragmentArgs by navArgs() + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext()).setTitle("Удалить рецепт?") + .setMessage("Вы уверены, что хотите удалить рецепт «${args.recipeTitle}»?") + .setPositiveButton("Удалить") { _, _ -> + findNavController().getBackStackEntry(R.id.recipeFragment).savedStateHandle["DELETE_CONFIRMED"] = + true + }.setNegativeButton("Отмена", null).create() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeAdapter.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeAdapter.kt new file mode 100644 index 0000000..c59d872 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeAdapter.kt @@ -0,0 +1,76 @@ +package ru.otus.cookbook.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import coil.load +import ru.otus.cookbook.R +import ru.otus.cookbook.data.RecipeListItem +import ru.otus.cookbook.databinding.VhRecipeCategoryBinding +import ru.otus.cookbook.databinding.VhRecipeItemBinding + +class RecipeAdapter(private val onRecipeClick: (Int) -> Unit) : + RecyclerView.Adapter() { + + var items: List = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } + + companion object { + private const val TYPE_CATEGORY = 0 + private const val TYPE_ITEM = 1 + } + + override fun getItemViewType(position: Int): Int { + return when (items[position]) { + is RecipeListItem.CategoryItem -> TYPE_CATEGORY + is RecipeListItem.RecipeItem -> TYPE_ITEM + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return if (viewType == TYPE_CATEGORY) { + CategoryViewHolder(VhRecipeCategoryBinding.inflate(inflater, parent, false)) + } else { + RecipeViewHolder(VhRecipeItemBinding.inflate(inflater, parent, false)) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = items[position] + when (holder) { + is CategoryViewHolder -> holder.bind(item as RecipeListItem.CategoryItem) + is RecipeViewHolder -> { + val recipeItem = item as RecipeListItem.RecipeItem + holder.bind(recipeItem) + holder.itemView.setOnClickListener { onRecipeClick(recipeItem.id) } + } + } + } + + override fun getItemCount(): Int = items.size + + class CategoryViewHolder(private val binding: VhRecipeCategoryBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(item: RecipeListItem.CategoryItem) { + binding.headerTitle.text = item.name + } + } + + class RecipeViewHolder(private val binding: VhRecipeItemBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(item: RecipeListItem.RecipeItem) { + binding.recipeTitle.text = item.title + binding.recipeDescription.text = item.description + binding.recipeImageThumbnail.load(item.imageUrl) { + transformations() + crossfade(true) + placeholder(R.drawable.ic_placeholder_shapes) + error(R.drawable.ic_placeholder_shapes) + } + } + } +} \ No newline at end of file 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..97fb6a7 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt @@ -9,54 +9,74 @@ 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 kotlinx.coroutines.launch import ru.otus.cookbook.data.Recipe import ru.otus.cookbook.databinding.FragmentRecipeBinding +import coil.load +import ru.otus.cookbook.R 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: RecipeFragmentArgs by navArgs() + private val recipeId: Int get() = args.recipeId private val binding = FragmentBindingDelegate(this) - private val model: RecipeFragmentViewModel by viewModels( - extrasProducer = { - MutableCreationExtras(defaultViewModelCreationExtras).apply { - set(RecipeFragmentViewModel.RECIPE_ID_KEY, recipeId) - } - }, - factoryProducer = { RecipeFragmentViewModel.Factory } - ) + private val model: RecipeFragmentViewModel by viewModels(extrasProducer = { + MutableCreationExtras(defaultViewModelCreationExtras).apply { + set(RecipeFragmentViewModel.RECIPE_ID_KEY, recipeId) + } + }, factoryProducer = { RecipeFragmentViewModel.Factory }) override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View = binding.bind( - container, - FragmentRecipeBinding::inflate - ) + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View = binding.bind(container, FragmentRecipeBinding::inflate) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + viewLifecycleOwner.lifecycleScope.launch { - model.recipe - .flowWithLifecycle(viewLifecycleOwner.lifecycle) - .collect(::displayRecipe) + model.recipe.flowWithLifecycle(viewLifecycleOwner.lifecycle).collect(::displayRecipe) } - } - /** - * Use to get recipe title and pass to confirmation dialog - */ - private fun getTitle(): String { - return model.recipe.value.title - } + val navBackStackEntry = findNavController().getBackStackEntry(R.id.recipeFragment) + navBackStackEntry.savedStateHandle.getLiveData("DELETE_CONFIRMED") + .observe(viewLifecycleOwner) { isConfirmed -> + if (isConfirmed == true) { + navBackStackEntry.savedStateHandle.remove("DELETE_CONFIRMED") + model.delete() + if (!findNavController().popBackStack(R.id.cookbookFragment, false)) { + findNavController().popBackStack() + } + } + } - private fun displayRecipe(recipe: Recipe) { - // Display the recipe + binding.withBinding { + btnDelete.setOnClickListener { + val action = + RecipeFragmentDirections.actionRecipeFragmentToDeleteRecipeDialogFragment( + getTitle() + ) + findNavController().navigate(action) + } + btnBack.setOnClickListener { + findNavController().navigateUp() + } + } } - private fun deleteRecipe() { - model.delete() + private fun displayRecipe(recipe: Recipe) = binding.withBinding { + toolbarRecipeTitle.text = recipe.title + recipeTitle.text = recipe.title + recipeSubhead.text = recipe.description + recipeDescription.text = recipe.steps.joinToString("\n\n") { "• $it" } + recipeImage.load(recipe.imageUrl) { + crossfade(true) + placeholder(R.drawable.ic_placeholder_cookbook) + error(R.drawable.ic_placeholder_cookbook) + } } + + private fun getTitle(): String = model.recipe.value.title } \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_left.xml b/app/src/main/res/anim/slide_in_left.xml new file mode 100644 index 0000000..721ccc2 --- /dev/null +++ b/app/src/main/res/anim/slide_in_left.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..4038baf --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..9c973f7 --- /dev/null +++ b/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml new file mode 100644 index 0000000..8d635ee --- /dev/null +++ b/app/src/main/res/anim/slide_out_right.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000..96ad5b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..cdc19c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000..d08741d --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_placeholder_cookbook.png b/app/src/main/res/drawable/ic_placeholder_cookbook.png new file mode 100644 index 0000000..dbd489d Binary files /dev/null and b/app/src/main/res/drawable/ic_placeholder_cookbook.png differ diff --git a/app/src/main/res/drawable/ic_placeholder_shapes.png b/app/src/main/res/drawable/ic_placeholder_shapes.png new file mode 100644 index 0000000..9bf5f3d Binary files /dev/null and b/app/src/main/res/drawable/ic_placeholder_shapes.png differ diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..b92dcb8 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.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..e990a3d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,13 +7,12 @@ android:layout_height="match_parent" tools:context=".MainActivity"> - + \ 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..9d6365d 100644 --- a/app/src/main/res/layout/fragment_cookbook.xml +++ b/app/src/main/res/layout/fragment_cookbook.xml @@ -1,6 +1,64 @@ + android:layout_height="match_parent" + android:background="#FFFBFE"> + + + + + + + + + + + + + + \ 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..34b0841 100644 --- a/app/src/main/res/layout/fragment_recipe.xml +++ b/app/src/main/res/layout/fragment_recipe.xml @@ -1,6 +1,125 @@ + android:layout_height="match_parent" + android:background="#FEF7FF"> + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..b0a07ea 100644 --- a/app/src/main/res/layout/vh_recipe_category.xml +++ b/app/src/main/res/layout/vh_recipe_category.xml @@ -1,6 +1,14 @@ - - - \ No newline at end of file + \ 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..ff30950 100644 --- a/app/src/main/res/layout/vh_recipe_item.xml +++ b/app/src/main/res/layout/vh_recipe_item.xml @@ -1,6 +1,93 @@ - + - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/navigation.xml b/app/src/main/res/navigation/navigation.xml new file mode 100644 index 0000000..399b215 --- /dev/null +++ b/app/src/main/res/navigation/navigation.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index d5b075a..69a9dc4 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,7 +1,3 @@ - \ 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..a6b3dae 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,5 +1,2 @@ - - #FF000000 - #FFFFFFFF - \ No newline at end of file + \ 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..ad28f65 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ Cookbook + Удалить \ 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..654ecf0 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -6,4 +6,9 @@ \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 515d8d1..752591f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] agp = "8.7.3" +coil = "2.7.0" kotlin = "2.1.0" coreKtx = "1.15.0" fragmentKtx = "1.8.5" @@ -18,6 +19,7 @@ datastore = "1.1.1" [libraries] +coil = { module = "io.coil-kt:coil", version.ref = "coil" } kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } diff --git a/webinar/src/main/res/values/colors.xml b/webinar/src/main/res/values/colors.xml index c8524cd..a6b3dae 100644 --- a/webinar/src/main/res/values/colors.xml +++ b/webinar/src/main/res/values/colors.xml @@ -1,5 +1,2 @@ - - #FF000000 - #FFFFFFFF - \ No newline at end of file + \ No newline at end of file diff --git a/webinar/src/main/res/values/strings.xml b/webinar/src/main/res/values/strings.xml index d1f6a33..f0b0647 100644 --- a/webinar/src/main/res/values/strings.xml +++ b/webinar/src/main/res/values/strings.xml @@ -4,8 +4,6 @@ Dashboard Notifications This is my home: %s - This is notifications Fragment - This is dashboard Fragment Login Password Login