diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f3207a6..6a4d002 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.kotlin.dsl.annotationProcessor + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -44,6 +46,8 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.navigation.fragment) + implementation("com.github.bumptech.glide:glide:4.16.0") + annotationProcessor("com.github.bumptech.glide:compiler:4.16.0") implementation(libs.androidx.navigation.ui) implementation(libs.material) implementation(libs.androidx.activity) 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 +31,7 @@ class CookbookFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setupAppBar() setupRecyclerView() viewLifecycleOwner.lifecycleScope.launch { model.recipeList @@ -36,11 +40,25 @@ 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..d15bb2a --- /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 + } +} \ 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..75dc8b5 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,32 @@ package ru.otus.cookbook.ui import android.os.Bundle +import android.util.Log import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater 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.R import ru.otus.cookbook.data.Recipe import ru.otus.cookbook.databinding.FragmentRecipeBinding +import ru.otus.cookbook.ui.RecipeFragmentArgs 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 +49,12 @@ 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() + observeLiveData() viewLifecycleOwner.lifecycleScope.launch { model.recipe .flowWithLifecycle(viewLifecycleOwner.lifecycle) @@ -45,6 +62,18 @@ class RecipeFragment : Fragment() { } } + fun observeLiveData(){ + val navController = findNavController() + navController.currentBackStackEntry?.savedStateHandle?.getLiveData( + DeleteRecipeDialogFragment.DIALOG_RESULT_KEY + )?.observe(viewLifecycleOwner) { + if (it) { + deleteRecipe() + navController.navigateUp() + } + } + } + /** * Use to get recipe title and pass to confirmation dialog */ @@ -53,7 +82,32 @@ class RecipeFragment : Fragment() { } private fun displayRecipe(recipe: Recipe) { - // Display the recipe + binding.withBinding { + textViewTittle.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() { 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..15df250 --- /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.ViewHolder +import ru.otus.cookbook.databinding.VhRecipeItemBinding + +class RecipeItemViewHolder(val binding: VhRecipeItemBinding): 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..9b985b6 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipesAdapter.kt @@ -0,0 +1,64 @@ +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 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_RECIPE -> { + val binding = VhRecipeItemBinding.inflate(inflater, parent, false) + RecipeItemViewHolder(binding) + } + VIEW_TYPE_CATEGORY -> { + val binding = VhRecipeCategoryBinding.inflate(inflater, parent, false) + RecipeCategoryViewHolder(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 recipe = item as RecipeListItem.RecipeItem + with(holder){ + binding.textViewAvatar.text = recipe.title.first().toString() + binding.textViewTittle.text = recipe.title + binding.textViewDescription.text = recipe.description + Glide.with(binding.root.context) + .load(recipe.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_RECIPE = 100 + private const val VIEW_TYPE_CATEGORY = 101 + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_left.xml b/app/src/main/res/drawable/arrow_left.xml new file mode 100644 index 0000000..c6c34d6 --- /dev/null +++ b/app/src/main/res/drawable/arrow_left.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/border_gray.xml b/app/src/main/res/drawable/border_gray.xml new file mode 100644 index 0000000..683ae8c --- /dev/null +++ b/app/src/main/res/drawable/border_gray.xml @@ -0,0 +1,8 @@ + + + + + + \ 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..5281740 --- /dev/null +++ b/app/src/main/res/drawable/close.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/oval_purple.xml b/app/src/main/res/drawable/oval_purple.xml new file mode 100644 index 0000000..7f64346 --- /dev/null +++ b/app/src/main/res/drawable/oval_purple.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file 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_can_outline.xml b/app/src/main/res/drawable/trash_can_outline.xml new file mode 100644 index 0000000..c9c69f9 --- /dev/null +++ b/app/src/main/res/drawable/trash_can_outline.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..5cd7f13 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,19 @@ - - + - \ 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..133905b 100644 --- a/app/src/main/res/layout/fragment_recipe.xml +++ b/app/src/main/res/layout/fragment_recipe.xml @@ -1,6 +1,75 @@ - + 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..11d881c 100644 --- a/app/src/main/res/layout/vh_recipe_category.xml +++ b/app/src/main/res/layout/vh_recipe_category.xml @@ -1,6 +1,29 @@ + 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..5da15f8 100644 --- a/app/src/main/res/layout/vh_recipe_item.xml +++ b/app/src/main/res/layout/vh_recipe_item.xml @@ -1,6 +1,72 @@ + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools" + app:cardCornerRadius="10dp" + android:background="@drawable/border_gray" + android:layout_marginBottom="10dp" + android:layout_marginHorizontal="12dp" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml new file mode 100644 index 0000000..13e6c12 --- /dev/null +++ b/app/src/main/res/navigation/main_navigation.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + \ 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..0d48b52 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,9 @@ 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..751a430 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -6,4 +6,12 @@ + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 515d8d1..2bb9b02 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ junitVersion = "1.2.1" espressoCore = "3.6.1" serialization = "1.7.3" datastore = "1.1.1" +media3CommonKtx = "1.7.1" [libraries] @@ -37,6 +38,7 @@ androidx-activity = { group = "androidx.activity", name = "activity", version.re androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-media3-common-ktx = { group = "androidx.media3", name = "media3-common-ktx", version.ref = "media3CommonKtx" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/webinar/build.gradle.kts b/webinar/build.gradle.kts index d9a0c04..09fcce5 100644 --- a/webinar/build.gradle.kts +++ b/webinar/build.gradle.kts @@ -33,7 +33,7 @@ android { jvmTarget = "11" } buildFeatures { - viewBinding = true + dataBinding = true } } diff --git a/webinar/src/main/res/values/styles.xml b/webinar/src/main/res/values/styles.xml index b0055b1..47754ec 100644 --- a/webinar/src/main/res/values/styles.xml +++ b/webinar/src/main/res/values/styles.xml @@ -3,5 +3,6 @@ \ No newline at end of file