diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f3207a6..b5d14b0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -44,10 +44,12 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.navigation.fragment) + //api(libs.androidx.navigation.fragment.ktx) implementation(libs.androidx.navigation.ui) implementation(libs.material) implementation(libs.androidx.activity) implementation(libs.androidx.constraintlayout) + implementation(libs.coil) 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..81ce008 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,12 +2,14 @@ + + - diff --git a/app/src/main/ic_launcher_cook-playstore.png b/app/src/main/ic_launcher_cook-playstore.png new file mode 100644 index 0000000..e06dfd7 Binary files /dev/null and b/app/src/main/ic_launcher_cook-playstore.png differ diff --git a/app/src/main/kotlin/ru/otus/cookbook/data/RecipeRepository.kt b/app/src/main/kotlin/ru/otus/cookbook/data/RecipeRepository.kt index 52c89b8..6e91219 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/data/RecipeRepository.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/data/RecipeRepository.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update /** - * Provides access to the list of recipes. + * Предоставляет доступ к списку рецептов. */ class RecipeRepository(recipes: List) { @@ -16,9 +16,9 @@ class RecipeRepository(recipes: List) { private val recipes = MutableStateFlow(recipes) /** - * Returns the list of recipes as a flow. - * @param scope The coroutine scope to use for the flow. - * @param filter The filter to apply to the recipes. + * Возвращает список рецептов в виде потока. + * @param scope Область видимости корутины для потока. + * @param filter Фильтр для применения к рецептам. */ suspend fun getRecipes(scope: CoroutineScope, filter: RecipeFilter): StateFlow> = recipes .map { recipes -> recipes.asSequence() @@ -29,21 +29,23 @@ class RecipeRepository(recipes: List) { .stateIn(scope) /** - * Returns the list of categories as a flow. - * @param scope The coroutine scope to use for the flow. + * Возвращает список категорий в виде потока. + * @param scope Область видимости корутины для потока. */ suspend fun getCategories(scope: CoroutineScope): StateFlow> = recipes - .map { recipes -> recipes.map { it.category }.distinct().sorted() } + .map { recipes -> recipes.map { it.category }.distinct().sorted() } .stateIn(scope) /** - * Returns the recipe with the specified ID. + * Возвращает рецепт по указанному идентификатору. + * @param id Идентификатор рецепта. + * @return Рецепт с указанным идентификатором или null, если рецепт не найден. */ fun getRecipe(id: Int): Recipe? = recipes.value.find { it.id == id } /** - * Returns the list of recipes as a flow. - * @param id The ID of the recipe to delete. + * Удаляет рецепт с указанным идентификатором. + * @param id Идентификатор рецепта для удаления. */ fun deleteRecipe(id: Int) { recipes.update { list -> @@ -52,8 +54,8 @@ class RecipeRepository(recipes: List) { } /** - * Adds a recipe to the list. - * @param recipe The recipe to add. + * Добавляет новый рецепт в список. + * @param recipe Рецепт для добавления. */ fun addRecipe(recipe: Recipe) { recipes.update { list -> diff --git a/app/src/main/kotlin/ru/otus/cookbook/data/Utils.kt b/app/src/main/kotlin/ru/otus/cookbook/data/Utils.kt new file mode 100644 index 0000000..225de18 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/data/Utils.kt @@ -0,0 +1,31 @@ +package ru.otus.cookbook.data + +import android.util.Log +import android.widget.ImageView +import coil.load +import coil.request.CachePolicy +import coil.request.ErrorResult +import coil.request.ImageRequest +import coil.request.SuccessResult +import coil.transform.Transformation +import ru.otus.cookbook.R + +fun loadImage(imageView: ImageView, imageUrl: String, vararg transformations: Transformation) { + imageView.load(imageUrl) { + setHeader("User-Agent", "Mozilla/5.0") + placeholder(R.drawable.cart_item_icon) + error(R.drawable.ic_launcher_background) + transformations(*transformations) // Optional: Apply transformations + memoryCachePolicy(CachePolicy.ENABLED) // Optional: Enable memory caching + diskCachePolicy(CachePolicy.ENABLED) // Optional: Enable disk caching + listener( + onSuccess = { request: ImageRequest, result: SuccessResult -> + Log.d("Coil", "Image loaded successfully from ${result.dataSource}") + }, + onError = { request: ImageRequest, result: ErrorResult -> + Log.e("Coil", "Image load failed: ${result.throwable.message}") + imageView.setImageResource(R.drawable.ic_launcher_background) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt index efe6939..e490091 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt @@ -4,28 +4,49 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration import kotlinx.coroutines.launch import ru.otus.cookbook.data.RecipeListItem import ru.otus.cookbook.databinding.FragmentCookbookBinding -class CookbookFragment : Fragment() { +/** + * Фрагмент для отображения списка рецептов + */ +class CookbookFragment : Fragment(), ItemListener { private val binding = FragmentBindingDelegate(this) private val model: CookbookFragmentViewModel by viewModels { CookbookFragmentViewModel.Factory } + private val recipeListDiffAdapter: RecipeListDiffAdapter by lazy { RecipeListDiffAdapter(this) } + + + /** + * Создает и возвращает представление фрагмента + * + * @param inflater Объект LayoutInflater для создания представления + * @param container Родительская ViewGroup + * @param savedInstanceState Сохраненное состояние фрагмента + * @return Представление фрагмента + */ override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View = binding.bind( - container, - FragmentCookbookBinding::inflate - ) + ): View = binding.bind(container, FragmentCookbookBinding::inflate) + /** + * Вызывается после создания представления фрагмента + * + * @param view Представление фрагмента + * @param savedInstanceState Сохраненное состояние фрагмента + */ override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupRecyclerView() @@ -36,11 +57,44 @@ class CookbookFragment : Fragment() { } } + /** + * Настраивает RecyclerView для отображения списка рецептов + */ private fun setupRecyclerView() = binding.withBinding { - // Setup RecyclerView + recycleView.addItemDecoration( + DividerItemDecoration( + this@CookbookFragment.requireActivity(), + LinearLayout.VERTICAL + ) + ) + recycleView.adapter = recipeListDiffAdapter } + /** + * Обновляет список рецептов в адаптере + * + * @param recipeList Новый список рецептов для отображения + */ private fun onRecipeListUpdated(recipeList: List) { - // Handle recipe list + recipeListDiffAdapter.submitList(recipeList) + } + + /** + * Обрабатывает нажатие на элемент списка рецептов + * + * @param id Идентификатор выбранного рецепта + */ + override fun onItemClick(id: Int) { + findNavController() + .navigate(CookbookFragmentDirections.actionOpenRecipe(id)) + } + + /** + * Обрабатывает свайп по элементу списка рецептов + * + * @param id Идентификатор рецепта, по которому был совершен свайп + */ + override fun onSwipe(id: Int) { + Toast.makeText(requireContext(), "Swiped $id", Toast.LENGTH_SHORT).show() } } \ No newline at end of file diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/ItemListener.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/ItemListener.kt new file mode 100644 index 0000000..fcd7d3e --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/ItemListener.kt @@ -0,0 +1,9 @@ +package ru.otus.cookbook.ui + +import java.util.UUID + +interface ItemListener { + fun onItemClick(id: Int) + fun onSwipe(id: Int) + +} \ 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..46d38eb 100644 --- a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeFragment.kt @@ -2,20 +2,30 @@ package ru.otus.cookbook.ui import android.os.Bundle import android.view.LayoutInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.MutableCreationExtras +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import coil.transform.RoundedCornersTransformation import kotlinx.coroutines.launch +import ru.otus.cookbook.R import ru.otus.cookbook.data.Recipe +import ru.otus.cookbook.data.loadImage import ru.otus.cookbook.databinding.FragmentRecipeBinding +import ru.otus.cookbook.ui.dialog.DeleteConfirmationDialog.Companion.CONFIRMATION_RESULT 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( @@ -31,13 +41,16 @@ class RecipeFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View = binding.bind( - container, - FragmentRecipeBinding::inflate - ) + ): View = binding.bind(container, FragmentRecipeBinding::inflate) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setupAlertDeleteResult() + binding.withBinding { + topAppBar.setNavigationOnClickListener(::navigateBackToCookBook) + topAppBar.setOnMenuItemClickListener(::navigateToRemoveDialog) + } viewLifecycleOwner.lifecycleScope.launch { model.recipe .flowWithLifecycle(viewLifecycleOwner.lifecycle) @@ -45,6 +58,18 @@ class RecipeFragment : Fragment() { } } + private fun navigateToRemoveDialog(menuItem: MenuItem): Boolean = + if (menuItem.itemId == R.id.menu_delete) { + findNavController() + .navigate(RecipeFragmentDirections.actionOpenDeleteConfirmationDialog(getTitle())) + true + } else false + + + private fun navigateBackToCookBook(v: View?) { + findNavController().navigate(RecipeFragmentDirections.actionBackToCookbook()) + } + /** * Use to get recipe title and pass to confirmation dialog */ @@ -53,7 +78,44 @@ class RecipeFragment : Fragment() { } private fun displayRecipe(recipe: Recipe) { - // Display the recipe + binding.withBinding { + if (recipe.imageUrl.isNotEmpty()) { + loadImage(recipeAvatar, recipe.imageUrl, RoundedCornersTransformation()) + } else { + recipeAvatar.setImageResource(R.drawable.cart_item_icon) + } + + recipeTitle.text = recipe.title.ifEmpty { getString(R.string.no_title) } + recipeDescription.text = + recipe.description.ifEmpty { getString(R.string.no_description) } + + recipeStep.text = if (recipe.steps.isNotEmpty()) { + recipe.steps.joinToString("\n • ", "• ") + } else { + getString(R.string.no_steps) + } + } + } + + private fun setupAlertDeleteResult() { + val navBackStackEntry = findNavController().getBackStackEntry(R.id.recipeFragment) + val observer = object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + if (navBackStackEntry.savedStateHandle.contains(CONFIRMATION_RESULT)) { + if (true == navBackStackEntry.savedStateHandle.get(CONFIRMATION_RESULT)) + deleteRecipe() + findNavController().popBackStack() + } + } + } + + navBackStackEntry.lifecycle.addObserver(observer) + + viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + navBackStackEntry.lifecycle.removeObserver(observer) + } + }) } private fun deleteRecipe() { diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeListDiffAdapter.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeListDiffAdapter.kt new file mode 100644 index 0000000..3414c7d --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeListDiffAdapter.kt @@ -0,0 +1,100 @@ +package ru.otus.cookbook.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import ru.otus.cookbook.data.RecipeListItem +import ru.otus.cookbook.databinding.VhRecipeCategoryBinding +import ru.otus.cookbook.databinding.VhRecipeItemBinding + +/** + * Адаптер для отображения списка рецептов с использованием DiffUtil для оптимизации обновлений + */ +class RecipeListDiffAdapter( + private val itemListener: ItemListener, +) : ListAdapter(DiffUtilItem()) { + + /** + * Создает ViewHolder для элемента списка в зависимости от типа элемента + * + * @param parent Родительская ViewGroup + * @param viewType Тип представления элемента + * @return ViewHolder для соответствующего типа элемента + */ + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + RecipeListItem.RecipeItem.layoutId -> + RecipeViewHolder(VhRecipeItemBinding.inflate(inflater, parent, false), itemListener) + + RecipeListItem.CategoryItem.layoutId -> + CategoryViewHolder(VhRecipeCategoryBinding.inflate(inflater, parent, false)) + + else -> throw IllegalArgumentException("Unknown view type") + } + } + + /** + * Привязывает данные элемента к ViewHolder + * + * @param holder ViewHolder для привязки данных + * @param position Позиция элемента в списке + */ + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int + ) { + when (val item = getItem(position)) { + is RecipeListItem.RecipeItem -> (holder as RecipeViewHolder).bind(item) + is RecipeListItem.CategoryItem -> (holder as CategoryViewHolder).bind(item) + else -> throw IllegalArgumentException("Unknown item type") + } + } + + /** + * Возвращает тип представления для элемента в указанной позиции + * + * @param position Позиция элемента в списке + * @return Тип представления элемента + */ + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is RecipeListItem.RecipeItem -> RecipeListItem.RecipeItem.layoutId + is RecipeListItem.CategoryItem -> RecipeListItem.CategoryItem.layoutId + else -> -1 + } + } +} + +/** + * Класс для сравнения элементов списка при обновлении данных + */ +private class DiffUtilItem : DiffUtil.ItemCallback() { + + /** + * Проверяет, представляют ли два объекта один и тот же элемент + * + * @param oldItem Старый элемент + * @param newItem Новый элемент + * @return true, если элементы представляют один и тот же объект, иначе false + */ + override fun areItemsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean { + return oldItem::class == newItem::class && oldItem.layoutId == newItem.layoutId + } + + /** + * Проверяет, содержат ли два объекта одинаковые данные + * + * @param oldItem Старый элемент + * @param newItem Новый элемент + * @return true, если данные элементов идентичны, иначе false + */ + override fun areContentsTheSame(oldItem: RecipeListItem, newItem: RecipeListItem): Boolean { + return oldItem == newItem + } +} diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeViewHolder.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeViewHolder.kt new file mode 100644 index 0000000..9f2a40c --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/RecipeViewHolder.kt @@ -0,0 +1,47 @@ +package ru.otus.cookbook.ui + +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import coil.transform.CircleCropTransformation +import ru.otus.cookbook.data.RecipeListItem +import ru.otus.cookbook.data.loadImage +import ru.otus.cookbook.databinding.VhRecipeCategoryBinding +import ru.otus.cookbook.databinding.VhRecipeItemBinding + + +class RecipeViewHolder( + private val binding: VhRecipeItemBinding, + private val itemListener: ItemListener, +) : RecyclerView.ViewHolder(binding.root) { + + private val recipeAvatar: ImageView = binding.recipeAvatar + private val recipeTitle: TextView = binding.recipeTitle + private val recipeDescription: TextView = binding.recipeDescription + + fun bind(recipe: RecipeListItem.RecipeItem) { + with(recipe) { + loadImage(recipeAvatar, imageUrl, CircleCropTransformation()) + recipeTitle.text = title + recipeDescription.text = description + binding.root.setOnClickListener { + itemListener.onItemClick(id) + } + } + } + +} + + +class CategoryViewHolder( + binding: VhRecipeCategoryBinding, +) : RecyclerView.ViewHolder(binding.root) { + + private val categoryName: TextView = binding.categoryName + + fun bind(category: RecipeListItem.CategoryItem) { + with(category) { + categoryName.text = name + } + } +} diff --git a/app/src/main/kotlin/ru/otus/cookbook/ui/dialog/DeleteConfirmationDialog.kt b/app/src/main/kotlin/ru/otus/cookbook/ui/dialog/DeleteConfirmationDialog.kt new file mode 100644 index 0000000..f39d9d0 --- /dev/null +++ b/app/src/main/kotlin/ru/otus/cookbook/ui/dialog/DeleteConfirmationDialog.kt @@ -0,0 +1,40 @@ +package ru.otus.cookbook.ui.dialog + +import android.app.AlertDialog +import android.app.Dialog +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import androidx.navigation.fragment.findNavController +import ru.otus.cookbook.R + +/** + * Диалог подтверждения удаления рецепта + */ +class DeleteConfirmationDialog : DialogFragment() { + companion object { + const val CONFIRMATION_RESULT = "delete_confirmation_result" + } + + private val recipeName get() = DeleteConfirmationDialogArgs.fromBundle(requireArguments()).recipeName + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + AlertDialog.Builder(requireContext()) + .setMessage(getString(R.string.delete_recipe_question) + " " + recipeName + "?") + .setTitle(getString(R.string.delete_dialog_title)) + .setPositiveButton(getString(android.R.string.ok)) { _, _ -> + dismiss() + setResult(true) + } + .setNegativeButton(getString(android.R.string.cancel)) { _, _ -> + dismiss() + setResult(false) + } + .create() + + private fun setResult(result: Boolean) { + with(findNavController()) { + previousBackStackEntry?.savedStateHandle?.set(CONFIRMATION_RESULT, result) + findNavController().popBackStack() + } + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml new file mode 100644 index 0000000..508ed72 --- /dev/null +++ b/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml new file mode 100644 index 0000000..03533cc --- /dev/null +++ b/app/src/main/res/anim/fade_out.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in.xml b/app/src/main/res/anim/slide_in.xml new file mode 100644 index 0000000..f36cc69 --- /dev/null +++ b/app/src/main/res/anim/slide_in.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out.xml b/app/src/main/res/anim/slide_out.xml new file mode 100644 index 0000000..3e7805c --- /dev/null +++ b/app/src/main/res/anim/slide_out.xml @@ -0,0 +1,6 @@ + + \ 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..6935245 --- /dev/null +++ b/app/src/main/res/drawable/arrow_left.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cart_item_icon.xml b/app/src/main/res/drawable/cart_item_icon.xml new file mode 100644 index 0000000..f2987f4 --- /dev/null +++ b/app/src/main/res/drawable/cart_item_icon.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_cook_background.xml b/app/src/main/res/drawable/ic_launcher_cook_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_cook_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_cook_foreground.xml b/app/src/main/res/drawable/ic_launcher_cook_foreground.xml new file mode 100644 index 0000000..e8e5da8 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_cook_foreground.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/remove_icon.xml b/app/src/main/res/drawable/remove_icon.xml new file mode 100644 index 0000000..a74d7a1 --- /dev/null +++ b/app/src/main/res/drawable/remove_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..b2d57a6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,13 +7,13 @@ 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..269be1e 100644 --- a/app/src/main/res/layout/fragment_cookbook.xml +++ b/app/src/main/res/layout/fragment_cookbook.xml @@ -1,6 +1,19 @@ + android:layout_height="match_parent" + android:paddingVertical="20dp" + tools:context="ru.otus.cookbook.ui.CookbookFragment"> + \ 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..d0aabdd 100644 --- a/app/src/main/res/layout/fragment_recipe.xml +++ b/app/src/main/res/layout/fragment_recipe.xml @@ -1,6 +1,74 @@ + android:layout_height="match_parent" + tools:context="ru.otus.cookbook.ui.RecipeFragment"> + + + + + + + + + + + + + + \ 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..3ba4482 100644 --- a/app/src/main/res/layout/vh_recipe_category.xml +++ b/app/src/main/res/layout/vh_recipe_category.xml @@ -1,6 +1,17 @@ + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:app="http://schemas.android.com/apk/res-auto" + 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..86da9d5 100644 --- a/app/src/main/res/layout/vh_recipe_item.xml +++ b/app/src/main/res/layout/vh_recipe_item.xml @@ -1,6 +1,64 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:backgroundTint="@color/black" + android:backgroundTintMode="screen" + android:paddingVertical="12dp" + android:paddingStart="16dp" + android:paddingEnd="24dp"> + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/recipe_app_bar.xml b/app/src/main/res/menu/recipe_app_bar.xml new file mode 100644 index 0000000..146af15 --- /dev/null +++ b/app/src/main/res/menu/recipe_app_bar.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_cook.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_cook.xml new file mode 100644 index 0000000..3983507 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_cook.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_cook_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_cook_round.xml new file mode 100644 index 0000000..3983507 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_cook_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_cook.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_cook.webp new file mode 100644 index 0000000..53b5a62 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_cook.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_cook_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_cook_round.webp new file mode 100644 index 0000000..d7c147e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_cook_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_cook.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_cook.webp new file mode 100644 index 0000000..d175dff Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_cook.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_cook_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_cook_round.webp new file mode 100644 index 0000000..0a2a26e Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_cook_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_cook.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_cook.webp new file mode 100644 index 0000000..c1e1154 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_cook.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_cook_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_cook_round.webp new file mode 100644 index 0000000..2033dc7 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_cook_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_cook.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_cook.webp new file mode 100644 index 0000000..a7f681c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_cook.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_cook_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_cook_round.webp new file mode 100644 index 0000000..253bd0a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_cook_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_cook.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_cook.webp new file mode 100644 index 0000000..6212979 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_cook.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_cook_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_cook_round.webp new file mode 100644 index 0000000..6339a16 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_cook_round.webp differ diff --git a/app/src/main/res/navigation/recipe_graph.xml b/app/src/main/res/navigation/recipe_graph.xml new file mode 100644 index 0000000..a32673d --- /dev/null +++ b/app/src/main/res/navigation/recipe_graph.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + \ 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..d08e052 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,4 +2,6 @@ #FF000000 #FFFFFFFF + #FEF7FF + #1D1B20 \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_cook_background.xml b/app/src/main/res/values/ic_launcher_cook_background.xml new file mode 100644 index 0000000..67acb6b --- /dev/null +++ b/app/src/main/res/values/ic_launcher_cook_background.xml @@ -0,0 +1,4 @@ + + + #DED4AA + \ 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..2bccd0b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,12 @@ + Cookbook + + Без названия + Нет описания + Нет шагов приготовления + Вы действительно хотите удалить + Delete + Remove + Delete \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 515d8d1..0421eb0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,9 +2,9 @@ agp = "8.7.3" kotlin = "2.1.0" coreKtx = "1.15.0" -fragmentKtx = "1.8.5" +fragmentKtx = "1.8.9" lifecycleKtx = "2.8.7" -navigation = "2.8.5" +navigation = "2.9.6" junit = "4.13.2" appcompat = "1.7.0" material = "1.12.0" @@ -15,7 +15,7 @@ junitVersion = "1.2.1" espressoCore = "3.6.1" serialization = "1.7.3" datastore = "1.1.1" - +coil = "2.5.0" [libraries] kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } @@ -26,6 +26,7 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx- androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" } androidx-navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigation" } +androidx-navigation-fragment-ktx ={group="androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation"} androidx-navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigation" } navigation-safeargs-gradle = { group = "androidx.navigation", name="navigation-safe-args-gradle-plugin", version.ref = "navigation" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleKtx" } @@ -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" } +coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }