Add linked ingredients to recipe step (#177)
* Add Compose to app module * Move Theme to ui module * Add Coil image loader * Use Compose for recipe screen * Save instruction to ingredient relation to DB * Display ingredients as server formats them * Display linked ingredients under each step * Fix ingredients padding * Show recipe full screen * Fix recipe screen UI issues * Hide keyboard on recipe navigation * Fix loading recipes from DB with no instructions or ingredients * Add instructions section title * Add ingredients section title * Remove unused view holders
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package gq.kirmanak.mealient
|
||||
|
||||
import android.app.Application
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration
|
||||
@@ -24,6 +26,9 @@ class App : Application() {
|
||||
@Inject
|
||||
lateinit var migrationDetector: MigrationDetector
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private val appCoroutineScope = CoroutineScope(Dispatchers.Main + Job())
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -31,5 +36,6 @@ class App : Application() {
|
||||
logger.v { "onCreate() called" }
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
appCoroutineScope.launch { migrationDetector.executeMigrations() }
|
||||
Coil.setImageLoader(imageLoader)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@ package gq.kirmanak.mealient.data.recipes.impl
|
||||
|
||||
interface RecipeImageUrlProvider {
|
||||
|
||||
suspend fun generateImageUrl(slug: String?): String?
|
||||
suspend fun generateImageUrl(imageId: String?): String?
|
||||
}
|
||||
@@ -10,10 +10,10 @@ class RecipeImageUrlProviderImpl @Inject constructor(
|
||||
private val logger: Logger,
|
||||
) : RecipeImageUrlProvider {
|
||||
|
||||
override suspend fun generateImageUrl(slug: String?): String? {
|
||||
logger.v { "generateImageUrl() called with: slug = $slug" }
|
||||
slug?.takeUnless { it.isBlank() } ?: return null
|
||||
val imagePath = IMAGE_PATH_FORMAT.format(slug)
|
||||
override suspend fun generateImageUrl(imageId: String?): String? {
|
||||
logger.v { "generateImageUrl() called with: slug = $imageId" }
|
||||
imageId?.takeUnless { it.isBlank() } ?: return null
|
||||
val imagePath = IMAGE_PATH_FORMAT.format(imageId)
|
||||
val baseUrl = serverInfoRepo.getUrl()?.takeUnless { it.isEmpty() }
|
||||
val result = baseUrl
|
||||
?.takeUnless { it.isBlank() }
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.paging.PagingConfig
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
||||
import gq.kirmanak.mealient.database.recipe.RecipeStorage
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientToInstructionEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions
|
||||
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
||||
@@ -45,15 +46,24 @@ class RecipeRepoImpl @Inject constructor(
|
||||
override suspend fun refreshRecipeInfo(recipeSlug: String): Result<Unit> {
|
||||
logger.v { "refreshRecipeInfo() called with: recipeSlug = $recipeSlug" }
|
||||
return runCatchingExceptCancel {
|
||||
val info = dataSource.requestRecipe(recipeSlug)
|
||||
val entity = modelMapper.toRecipeEntity(info)
|
||||
val ingredients = info.recipeIngredients.map {
|
||||
val recipe = dataSource.requestRecipe(recipeSlug)
|
||||
val entity = modelMapper.toRecipeEntity(recipe)
|
||||
val ingredients = recipe.ingredients.map {
|
||||
modelMapper.toRecipeIngredientEntity(it, entity.remoteId)
|
||||
}
|
||||
val instructions = info.recipeInstructions.map {
|
||||
val instructions = recipe.instructions.map {
|
||||
modelMapper.toRecipeInstructionEntity(it, entity.remoteId)
|
||||
}
|
||||
storage.saveRecipeInfo(entity, ingredients, instructions)
|
||||
val ingredientToInstruction = recipe.instructions.flatMap { instruction ->
|
||||
instruction.ingredientReferences.map { ingredientReference ->
|
||||
RecipeIngredientToInstructionEntity(
|
||||
recipeId = entity.remoteId,
|
||||
ingredientId = ingredientReference.referenceId,
|
||||
instructionId = instruction.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
storage.saveRecipeInfo(entity, ingredients, instructions, ingredientToInstruction)
|
||||
}.onFailure {
|
||||
logger.e(it) { "loadRecipeInfo: can't update full recipe info" }
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import coil.ImageLoader
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -13,6 +14,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import gq.kirmanak.mealient.data.storage.PreferencesStorage
|
||||
import gq.kirmanak.mealient.data.storage.PreferencesStorageImpl
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -24,6 +26,17 @@ interface AppModule {
|
||||
@Singleton
|
||||
fun provideDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
|
||||
PreferenceDataStoreFactory.create { context.preferencesDataStoreFile("settings") }
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCoilImageLoader(
|
||||
@ApplicationContext context: Context,
|
||||
okHttpClient: OkHttpClient,
|
||||
): ImageLoader {
|
||||
return ImageLoader.Builder(context)
|
||||
.okHttpClient(okHttpClient)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Binds
|
||||
|
||||
@@ -82,6 +82,7 @@ class RecipesListFragment : Fragment(R.layout.fragment_recipes_list) {
|
||||
private fun navigateToRecipeInfo(id: String) {
|
||||
logger.v { "navigateToRecipeInfo() called with: id = $id" }
|
||||
val directions = actionRecipesFragmentToRecipeInfoFragment(id)
|
||||
binding.root.hideKeyboard()
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,79 +4,52 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.databinding.FragmentRecipeInfoBinding
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
||||
import gq.kirmanak.mealient.ui.AppTheme
|
||||
import gq.kirmanak.mealient.ui.CheckableMenuItem
|
||||
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RecipeInfoFragment : BottomSheetDialogFragment() {
|
||||
class RecipeInfoFragment : Fragment() {
|
||||
|
||||
private val binding by viewBinding(FragmentRecipeInfoBinding::bind)
|
||||
private val viewModel by viewModels<RecipeInfoViewModel>()
|
||||
private lateinit var ingredientsAdapter: RecipeIngredientsAdapter
|
||||
|
||||
@Inject
|
||||
lateinit var instructionsAdapter: RecipeInstructionsAdapter
|
||||
|
||||
@Inject
|
||||
lateinit var recipeIngredientsAdapterFactory: RecipeIngredientsAdapter.Factory
|
||||
private val activityViewModel by activityViewModels<MainActivityViewModel>()
|
||||
|
||||
@Inject
|
||||
lateinit var logger: Logger
|
||||
|
||||
@Inject
|
||||
lateinit var recipeImageLoader: RecipeImageLoader
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
logger.v { "onCreateView() called" }
|
||||
return FragmentRecipeInfoBinding.inflate(inflater, container, false).root
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
AppTheme {
|
||||
RecipeScreen(uiState = uiState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
logger.v { "onViewCreated() called" }
|
||||
|
||||
with(binding) {
|
||||
instructionsList.adapter = instructionsAdapter
|
||||
}
|
||||
|
||||
with(viewModel) {
|
||||
uiState.observe(viewLifecycleOwner, ::onUiStateChange)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onUiStateChange(uiState: RecipeInfoUiState) = with(binding) {
|
||||
logger.v { "onUiStateChange() called" }
|
||||
if (::ingredientsAdapter.isInitialized.not()) {
|
||||
ingredientsAdapter = recipeIngredientsAdapterFactory.build(uiState.disableAmounts)
|
||||
ingredientsList.adapter = ingredientsAdapter
|
||||
}
|
||||
ingredientsHolder.isVisible = uiState.showIngredients
|
||||
instructionsGroup.isVisible = uiState.showInstructions
|
||||
recipeImageLoader.loadRecipeImage(image, uiState.summaryEntity)
|
||||
title.text = uiState.title
|
||||
description.text = uiState.description
|
||||
ingredientsAdapter.submitList(uiState.recipeIngredients)
|
||||
instructionsAdapter.submitList(uiState.recipeInstructions)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
logger.v { "onDestroyView() called" }
|
||||
// Prevent RV leaking through mObservers list in adapter
|
||||
with(binding) {
|
||||
ingredientsList.adapter = null
|
||||
instructionsList.adapter = null
|
||||
activityViewModel.updateUiState {
|
||||
it.copy(
|
||||
navigationVisible = false,
|
||||
searchVisible = false,
|
||||
checkedMenuItem = CheckableMenuItem.RecipesList,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ data class RecipeInfoUiState(
|
||||
val showInstructions: Boolean = false,
|
||||
val summaryEntity: RecipeSummaryEntity? = null,
|
||||
val recipeIngredients: List<RecipeIngredientEntity> = emptyList(),
|
||||
val recipeInstructions: List<RecipeInstructionEntity> = emptyList(),
|
||||
val recipeInstructions: Map<RecipeInstructionEntity, List<RecipeIngredientEntity>> = emptyMap(),
|
||||
val title: String? = null,
|
||||
val description: String? = null,
|
||||
val disableAmounts: Boolean = true,
|
||||
val imageUrl: String? = null,
|
||||
)
|
||||
|
||||
@@ -1,38 +1,66 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.info
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class RecipeInfoViewModel @Inject constructor(
|
||||
private val recipeRepo: RecipeRepo,
|
||||
private val logger: Logger,
|
||||
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
|
||||
private val args = RecipeInfoFragmentArgs.fromSavedStateHandle(savedStateHandle)
|
||||
|
||||
val uiState: LiveData<RecipeInfoUiState> = liveData {
|
||||
private val _uiState = flow {
|
||||
logger.v { "Initializing UI state with args = $args" }
|
||||
val state = recipeRepo.loadRecipeInfo(args.recipeId)?.let { entity ->
|
||||
val recipeInfo = recipeRepo.loadRecipeInfo(args.recipeId)
|
||||
logger.v { "Loaded recipe info = $recipeInfo" }
|
||||
val slug = recipeInfo?.recipeSummaryEntity?.imageId
|
||||
val imageUrl = slug?.let { recipeImageUrlProvider.generateImageUrl(slug) }
|
||||
val state = recipeInfo?.let { entity ->
|
||||
RecipeInfoUiState(
|
||||
showIngredients = entity.recipeIngredients.isNotEmpty(),
|
||||
showInstructions = entity.recipeInstructions.isNotEmpty(),
|
||||
summaryEntity = entity.recipeSummaryEntity,
|
||||
recipeIngredients = entity.recipeIngredients,
|
||||
recipeInstructions = entity.recipeInstructions,
|
||||
recipeInstructions = associateInstructionsToIngredients(entity),
|
||||
title = entity.recipeSummaryEntity.name,
|
||||
description = entity.recipeSummaryEntity.description,
|
||||
disableAmounts = entity.recipeEntity.disableAmounts,
|
||||
imageUrl = imageUrl,
|
||||
)
|
||||
} ?: RecipeInfoUiState()
|
||||
emit(state)
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, RecipeInfoUiState())
|
||||
|
||||
|
||||
val uiState: StateFlow<RecipeInfoUiState> = _uiState
|
||||
|
||||
}
|
||||
|
||||
private fun associateInstructionsToIngredients(
|
||||
recipe: RecipeWithSummaryAndIngredientsAndInstructions,
|
||||
): Map<RecipeInstructionEntity, List<RecipeIngredientEntity>> {
|
||||
return recipe.recipeInstructions.associateWith { instruction ->
|
||||
recipe.recipeIngredientToInstructionEntity
|
||||
.filter { it.instructionId == instruction.id }
|
||||
.flatMap { mapping ->
|
||||
recipe.recipeIngredients.filter { ingredient ->
|
||||
ingredient.id == mapping.ingredientId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.info
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.view.isGone
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
|
||||
import gq.kirmanak.mealient.databinding.ViewHolderIngredientBinding
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import gq.kirmanak.mealient.ui.recipes.info.RecipeIngredientsAdapter.RecipeIngredientViewHolder
|
||||
|
||||
class RecipeIngredientsAdapter @AssistedInject constructor(
|
||||
private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory,
|
||||
private val logger: Logger,
|
||||
@Assisted private val disableAmounts: Boolean,
|
||||
) : ListAdapter<RecipeIngredientEntity, RecipeIngredientViewHolder>(RecipeIngredientDiffCallback) {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun build(disableAmounts: Boolean): RecipeIngredientsAdapter
|
||||
}
|
||||
|
||||
class RecipeIngredientViewHolder @AssistedInject constructor(
|
||||
@Assisted private val binding: ViewHolderIngredientBinding,
|
||||
@Assisted private val disableAmounts: Boolean,
|
||||
private val logger: Logger,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun build(
|
||||
binding: ViewHolderIngredientBinding,
|
||||
disableAmounts: Boolean,
|
||||
): RecipeIngredientViewHolder
|
||||
}
|
||||
|
||||
fun bind(item: RecipeIngredientEntity) {
|
||||
logger.v { "bind() called with: item = $item" }
|
||||
binding.sectionGroup.isGone = item.title.isNullOrBlank()
|
||||
binding.title.text = item.title.orEmpty()
|
||||
binding.checkBox.text = if (disableAmounts) {
|
||||
item.note
|
||||
} else {
|
||||
val builder = StringBuilder()
|
||||
item.quantity?.let { builder.append("${it.formatUsingMediantMethod()} ") }
|
||||
item.unit?.let { builder.append("$it ") }
|
||||
item.food?.let { builder.append("$it ") }
|
||||
builder.append(item.note)
|
||||
builder.toString().trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" }
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return recipeIngredientViewHolderFactory.build(
|
||||
ViewHolderIngredientBinding.inflate(inflater, parent, false),
|
||||
disableAmounts,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecipeIngredientViewHolder, position: Int) {
|
||||
logger.v { "onBindViewHolder() called with: holder = $holder, position = $position" }
|
||||
val item = getItem(position)
|
||||
logger.d { "onBindViewHolder: item is $item" }
|
||||
holder.bind(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Double.formatUsingMediantMethod(d: Int = 10, mixed: Boolean = true): String {
|
||||
val triple = mediantMethod(d, mixed)
|
||||
return when {
|
||||
triple.second == 0 -> "${triple.first}"
|
||||
triple.first == 0 -> "${triple.second}/${triple.third}"
|
||||
else -> "${triple.first} ${triple.second}/${triple.third}"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rational approximation to a floating point number with bounded denominator using Mediant Method.
|
||||
* For example, 333/1000 will become [0, 1, 3] (1/3), 1414/1000 will be [1, 2, 5] (1 2/5).
|
||||
* Uses algorithm from this npm package - https://www.npmjs.com/package/frac
|
||||
* Can be seen here https://github.com/SheetJS/frac/blob/d07f3c99c7dc059fb47d391bcb3da80f4956608e/frac.js
|
||||
* @receiver - number that needs to be approximated
|
||||
* @param d - maximum denominator (i.e. if 10 then 17/20 will be 4/5, if 20 then 17/20).
|
||||
* @param mixed - if true returns a mixed fraction otherwise improper (i.e. "7/5" or "1 2/5")
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun Double.mediantMethod(d: Int = 10, mixed: Boolean = true): Triple<Int, Int, Int> {
|
||||
val x = this
|
||||
var n1 = x.toInt()
|
||||
var d1 = 1
|
||||
var n2 = n1 + 1
|
||||
var d2 = 1
|
||||
if (x != n1.toDouble()) {
|
||||
while (d1 <= d && d2 <= d) {
|
||||
val m = (n1 + n2).toDouble() / (d1 + d2)
|
||||
when {
|
||||
x == m -> {
|
||||
when {
|
||||
d1 + d2 <= d -> {
|
||||
d1 += d2
|
||||
n1 += n2
|
||||
d2 = d + 1
|
||||
}
|
||||
|
||||
d1 > d2 -> d2 = d + 1
|
||||
else -> d1 = d + 1
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
x < m -> {
|
||||
n2 += n1
|
||||
d2 += d1
|
||||
}
|
||||
|
||||
else -> {
|
||||
n1 += n2
|
||||
d1 += d2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (d1 > d) {
|
||||
d1 = d2
|
||||
n1 = n2
|
||||
}
|
||||
if (!mixed) return Triple(0, n1, d1)
|
||||
val q = (n1.toDouble() / d1).toInt()
|
||||
return Triple(q, n1 - q * d1, d1)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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 dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
|
||||
import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding
|
||||
import gq.kirmanak.mealient.extensions.resources
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import gq.kirmanak.mealient.ui.recipes.info.RecipeInstructionsAdapter.RecipeInstructionViewHolder
|
||||
import javax.inject.Inject
|
||||
|
||||
class RecipeInstructionsAdapter @Inject constructor(
|
||||
private val logger: Logger,
|
||||
private val recipeInstructionViewHolderFactory: RecipeInstructionViewHolder.Factory,
|
||||
) : ListAdapter<RecipeInstructionEntity, 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 @AssistedInject constructor(
|
||||
@Assisted private val binding: ViewHolderInstructionBinding,
|
||||
private val logger: Logger,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun build(binding: ViewHolderInstructionBinding): RecipeInstructionViewHolder
|
||||
}
|
||||
|
||||
fun bind(item: RecipeInstructionEntity, position: Int) {
|
||||
logger.v { "bind() called with: item = $item, position = $position" }
|
||||
binding.step.text = binding.resources.getString(
|
||||
R.string.view_holder_recipe_instructions_step, position + 1
|
||||
)
|
||||
binding.instruction.text = item.text
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeInstructionViewHolder {
|
||||
logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" }
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return recipeInstructionViewHolderFactory.build(
|
||||
ViewHolderInstructionBinding.inflate(inflater, parent, false),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecipeInstructionViewHolder, position: Int) {
|
||||
logger.v { "onBindViewHolder() called with: holder = $holder, position = $position" }
|
||||
val item = getItem(position)
|
||||
logger.d { "onBindViewHolder: item is $item" }
|
||||
holder.bind(item, position)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.info
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.ui.AppTheme
|
||||
import gq.kirmanak.mealient.ui.Dimens
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
|
||||
@Composable
|
||||
fun RecipeScreen(
|
||||
uiState: RecipeInfoUiState,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(
|
||||
state = rememberScrollState(),
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.Top),
|
||||
) {
|
||||
HeaderSection(
|
||||
imageUrl = uiState.imageUrl,
|
||||
title = uiState.title,
|
||||
description = uiState.description,
|
||||
)
|
||||
|
||||
if (uiState.showIngredients) {
|
||||
IngredientsSection(
|
||||
ingredients = uiState.recipeIngredients,
|
||||
)
|
||||
}
|
||||
|
||||
if (uiState.showInstructions) {
|
||||
InstructionsSection(
|
||||
instructions = uiState.recipeInstructions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HeaderSection(
|
||||
imageUrl: String?,
|
||||
title: String?,
|
||||
description: String?,
|
||||
) {
|
||||
val imageFallback = painterResource(id = R.drawable.placeholder_recipe)
|
||||
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(2f) // 2:1
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
topEnd = 0.dp,
|
||||
topStart = 0.dp,
|
||||
bottomEnd = Dimens.Intermediate,
|
||||
bottomStart = Dimens.Intermediate,
|
||||
)
|
||||
),
|
||||
model = imageUrl,
|
||||
contentDescription = stringResource(id = R.string.content_description_fragment_recipe_info_image),
|
||||
placeholder = imageFallback,
|
||||
error = imageFallback,
|
||||
fallback = imageFallback,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
|
||||
if (!title.isNullOrEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = Dimens.Small),
|
||||
text = title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
|
||||
if (!description.isNullOrEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = Dimens.Small),
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InstructionsSection(
|
||||
instructions: Map<RecipeInstructionEntity, List<RecipeIngredientEntity>>,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = Dimens.Large),
|
||||
text = stringResource(id = R.string.fragment_recipe_info_instructions_header),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
var stepCount = 0
|
||||
instructions.forEach { (instruction, ingredients) ->
|
||||
InstructionListItem(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = Dimens.Small),
|
||||
item = instruction,
|
||||
ingredients = ingredients,
|
||||
index = stepCount++,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IngredientsSection(
|
||||
ingredients: List<RecipeIngredientEntity>,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = Dimens.Large),
|
||||
text = stringResource(id = R.string.fragment_recipe_info_ingredients_header),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = Dimens.Small),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(Dimens.Small),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.Top),
|
||||
) {
|
||||
ingredients.forEach { item ->
|
||||
IngredientListItem(
|
||||
item = item,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InstructionListItem(
|
||||
item: RecipeInstructionEntity,
|
||||
index: Int,
|
||||
ingredients: List<RecipeIngredientEntity>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val title = item.title
|
||||
|
||||
if (!title.isNullOrBlank()) {
|
||||
Text(
|
||||
modifier = modifier
|
||||
.padding(horizontal = Dimens.Medium),
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(Dimens.Medium),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.Top),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.view_holder_recipe_instructions_step,
|
||||
index + 1
|
||||
),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = item.text.trim(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
if (ingredients.isNotEmpty()) {
|
||||
Divider(
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
ingredients.forEach { ingredient ->
|
||||
Text(
|
||||
text = ingredient.display,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IngredientListItem(
|
||||
item: RecipeIngredientEntity,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var isChecked by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val title = item.title
|
||||
if (!title.isNullOrBlank()) {
|
||||
Text(
|
||||
modifier = modifier
|
||||
.padding(horizontal = Dimens.Medium),
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
||||
Divider(
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.Start),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Checkbox(
|
||||
checked = isChecked,
|
||||
onCheckedChange = { isChecked = it },
|
||||
)
|
||||
|
||||
Text(
|
||||
text = item.display,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun RecipeScreenPreview() {
|
||||
AppTheme {
|
||||
RecipeScreen(
|
||||
uiState = previewUiState()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_MASK and UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun RecipeScreenNightPreview() {
|
||||
AppTheme {
|
||||
RecipeScreen(
|
||||
uiState = previewUiState()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun previewUiState(): RecipeInfoUiState {
|
||||
val ingredient = RecipeIngredientEntity(
|
||||
id = "2",
|
||||
recipeId = "1",
|
||||
note = "Recipe ingredient note",
|
||||
food = "Recipe ingredient food",
|
||||
unit = "Recipe ingredient unit",
|
||||
display = "Recipe ingredient display that is very long and should be wrapped",
|
||||
quantity = 1.0,
|
||||
title = null,
|
||||
)
|
||||
val uiState = RecipeInfoUiState(
|
||||
showIngredients = true,
|
||||
showInstructions = true,
|
||||
summaryEntity = RecipeSummaryEntity(
|
||||
remoteId = "1",
|
||||
name = "Recipe name",
|
||||
slug = "recipe-name",
|
||||
description = "Recipe description",
|
||||
dateAdded = LocalDate(2021, 1, 1),
|
||||
dateUpdated = LocalDateTime(2021, 1, 1, 1, 1, 1),
|
||||
imageId = null,
|
||||
isFavorite = false,
|
||||
),
|
||||
recipeIngredients = listOf(
|
||||
RecipeIngredientEntity(
|
||||
id = "1",
|
||||
recipeId = "1",
|
||||
note = "Recipe ingredient note",
|
||||
food = "Recipe ingredient food",
|
||||
unit = "Recipe ingredient unit",
|
||||
display = "Recipe ingredient display that is very long and should be wrapped",
|
||||
quantity = 1.0,
|
||||
title = "Recipe ingredient section title",
|
||||
),
|
||||
ingredient,
|
||||
),
|
||||
recipeInstructions = mapOf(
|
||||
RecipeInstructionEntity(
|
||||
id = "1",
|
||||
recipeId = "1",
|
||||
text = "Recipe instruction",
|
||||
title = "Section title",
|
||||
) to emptyList(),
|
||||
RecipeInstructionEntity(
|
||||
id = "2",
|
||||
recipeId = "1",
|
||||
text = "Recipe instruction",
|
||||
title = "",
|
||||
) to listOf(ingredient),
|
||||
),
|
||||
title = "Recipe title",
|
||||
description = "Recipe description",
|
||||
)
|
||||
return uiState
|
||||
}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true"
|
||||
tools:context=".ui.recipes.info.RecipeInfoFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/end_guide"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintGuide_end="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/start_guide"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintGuide_begin="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:contentDescription="@string/content_description_fragment_recipe_info_image"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toTopOf="@id/title"
|
||||
app:layout_constraintDimensionRatio="2:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearance="?shapeAppearanceCornerMedium"
|
||||
tools:srcCompat="@drawable/placeholder_recipe" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="7dp"
|
||||
android:textAppearance="?textAppearanceHeadline6"
|
||||
app:layout_constraintBottom_toTopOf="@id/description"
|
||||
app:layout_constraintEnd_toStartOf="@id/end_guide"
|
||||
app:layout_constraintStart_toEndOf="@id/start_guide"
|
||||
app:layout_constraintTop_toBottomOf="@id/image"
|
||||
tools:text="Best-Ever Beef Stew" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="6dp"
|
||||
android:textAppearance="?textAppearanceBody2"
|
||||
app:layout_constraintBottom_toTopOf="@id/ingredients_header"
|
||||
app:layout_constraintEnd_toStartOf="@id/end_guide"
|
||||
app:layout_constraintStart_toEndOf="@id/start_guide"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
tools:text="Stay warm all winter with this classic Beef Stew made with red wine and beef stock from Delish.com." />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ingredients_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="35dp"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:layout_marginEnd="@dimen/margin_small"
|
||||
android:text="@string/fragment_recipe_info_ingredients_header"
|
||||
android:textAppearance="?textAppearanceHeadline6"
|
||||
app:layout_constraintBottom_toTopOf="@id/ingredients_holder"
|
||||
app:layout_constraintEnd_toStartOf="@id/end_guide"
|
||||
app:layout_constraintStart_toEndOf="@id/start_guide"
|
||||
app:layout_constraintTop_toBottomOf="@id/description" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/ingredients_holder"
|
||||
style="?materialCardViewFilledStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="11dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/instructions_header"
|
||||
app:layout_constraintEnd_toStartOf="@id/end_guide"
|
||||
app:layout_constraintStart_toEndOf="@id/start_guide"
|
||||
app:layout_constraintTop_toBottomOf="@id/ingredients_header">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/ingredients_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/view_holder_ingredient" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/instructions_group"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="instructions_header,instructions_list" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/instructions_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="35dp"
|
||||
android:layout_marginEnd="@dimen/margin_small"
|
||||
android:text="@string/fragment_recipe_info_instructions_header"
|
||||
android:textAppearance="?textAppearanceHeadline6"
|
||||
app:layout_constraintBottom_toTopOf="@id/instructions_list"
|
||||
app:layout_constraintEnd_toStartOf="@id/end_guide"
|
||||
app:layout_constraintStart_toEndOf="@id/start_guide"
|
||||
app:layout_constraintTop_toBottomOf="@id/ingredients_holder" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/instructions_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/end_guide"
|
||||
app:layout_constraintStart_toEndOf="@id/start_guide"
|
||||
app:layout_constraintTop_toBottomOf="@id/instructions_header"
|
||||
tools:itemCount="2"
|
||||
tools:listitem="@layout/view_holder_instruction" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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">
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/section_group"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="title, title_divider"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:textAppearance="?textAppearanceHeadline6"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Meat a very long and weird title that someone might have for some reason" />
|
||||
|
||||
<View
|
||||
android:id="@+id/title_divider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:alpha="0.3"
|
||||
android:background="?colorOnSurface"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title_divider"
|
||||
tools:text="900 g braising steak/stew meat, cubed into 2.5cm pieces" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,38 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="?materialCardViewFilledStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_small">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="18dp"
|
||||
android:paddingVertical="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/step"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?textAppearanceHeadline6"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Step: 1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/instruction"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textAppearance="?textAppearanceBody2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/step"
|
||||
tools:text="In a large dutch oven or heavy-bottomed pot over medium heat, heat oil. Add beef and cook until seared on all sides, 10 minutes, working in batches if necessary. Transfer beef to a plate." />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -1,94 +1,97 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/nav_graph"
|
||||
tools:ignore="InvalidNavigation">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/nav_graph"
|
||||
tools:ignore="InvalidNavigation">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/authenticationFragment"
|
||||
android:name="gq.kirmanak.mealient.ui.auth.AuthenticationFragment"
|
||||
android:label="AuthenticationFragment"
|
||||
tools:layout="@layout/fragment_authentication" />
|
||||
<fragment
|
||||
android:id="@+id/authenticationFragment"
|
||||
android:name="gq.kirmanak.mealient.ui.auth.AuthenticationFragment"
|
||||
android:label="AuthenticationFragment"
|
||||
tools:layout="@layout/fragment_authentication" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/recipesListFragment"
|
||||
android:name="gq.kirmanak.mealient.ui.recipes.RecipesListFragment"
|
||||
android:label="fragment_recipes"
|
||||
tools:layout="@layout/fragment_recipes_list">
|
||||
<action
|
||||
android:id="@+id/action_recipesFragment_to_recipeInfoFragment"
|
||||
app:destination="@id/recipeInfoFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/recipesListFragment"
|
||||
android:name="gq.kirmanak.mealient.ui.recipes.RecipesListFragment"
|
||||
android:label="fragment_recipes"
|
||||
tools:layout="@layout/fragment_recipes_list">
|
||||
<action
|
||||
android:id="@+id/action_recipesFragment_to_recipeInfoFragment"
|
||||
app:destination="@id/recipeInfoFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
</fragment>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/recipeInfoFragment"
|
||||
android:name="gq.kirmanak.mealient.ui.recipes.info.RecipeInfoFragment"
|
||||
android:label="RecipeInfoFragment"
|
||||
tools:layout="@layout/fragment_recipe_info">
|
||||
<argument
|
||||
android:name="recipe_id"
|
||||
app:argType="string" />
|
||||
</dialog>
|
||||
<fragment
|
||||
android:id="@+id/recipeInfoFragment"
|
||||
android:name="gq.kirmanak.mealient.ui.recipes.info.RecipeInfoFragment"
|
||||
android:label="RecipeInfoFragment">
|
||||
<argument
|
||||
android:name="recipe_id"
|
||||
app:argType="string" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/disclaimerFragment"
|
||||
android:name="gq.kirmanak.mealient.ui.disclaimer.DisclaimerFragment"
|
||||
android:label="DisclaimerFragment"
|
||||
tools:layout="@layout/fragment_disclaimer">
|
||||
<action
|
||||
android:id="@+id/action_disclaimerFragment_to_baseURLFragment"
|
||||
app:destination="@id/baseURLFragment"
|
||||
app:popUpTo="@id/nav_graph"
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/disclaimerFragment"
|
||||
android:name="gq.kirmanak.mealient.ui.disclaimer.DisclaimerFragment"
|
||||
android:label="DisclaimerFragment"
|
||||
tools:layout="@layout/fragment_disclaimer">
|
||||
<action
|
||||
android:id="@+id/action_disclaimerFragment_to_baseURLFragment"
|
||||
app:destination="@id/baseURLFragment"
|
||||
app:popUpTo="@id/nav_graph"
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/baseURLFragment"
|
||||
android:name="gq.kirmanak.mealient.ui.baseurl.BaseURLFragment"
|
||||
android:label="fragment_base_url"
|
||||
tools:layout="@layout/fragment_base_url">
|
||||
<action
|
||||
android:id="@+id/action_baseURLFragment_to_recipesListFragment"
|
||||
app:destination="@id/recipesListFragment"
|
||||
app:popUpTo="@id/nav_graph" />
|
||||
<argument
|
||||
android:name="isOnboarding"
|
||||
app:argType="boolean" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/baseURLFragment"
|
||||
android:name="gq.kirmanak.mealient.ui.baseurl.BaseURLFragment"
|
||||
android:label="fragment_base_url"
|
||||
tools:layout="@layout/fragment_base_url">
|
||||
<action
|
||||
android:id="@+id/action_baseURLFragment_to_recipesListFragment"
|
||||
app:destination="@id/recipesListFragment"
|
||||
app:popUpTo="@id/nav_graph" />
|
||||
<argument
|
||||
android:name="isOnboarding"
|
||||
app:argType="boolean" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/addRecipeFragment"
|
||||
android:name="gq.kirmanak.mealient.ui.add.AddRecipeFragment"
|
||||
android:label="fragment_add_recipe"
|
||||
tools:layout="@layout/fragment_add_recipe" />
|
||||
<fragment
|
||||
android:id="@+id/addRecipeFragment"
|
||||
android:name="gq.kirmanak.mealient.ui.add.AddRecipeFragment"
|
||||
android:label="fragment_add_recipe"
|
||||
tools:layout="@layout/fragment_add_recipe" />
|
||||
|
||||
|
||||
<fragment
|
||||
android:id="@+id/shoppingListsFragment"
|
||||
android:name="gq.kirmanak.mealient.shopping_lists.ui.ShoppingListsFragment" />
|
||||
<fragment
|
||||
android:id="@+id/shoppingListsFragment"
|
||||
android:name="gq.kirmanak.mealient.shopping_lists.ui.ShoppingListsFragment" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_global_authenticationFragment"
|
||||
app:destination="@id/authenticationFragment" />
|
||||
<action
|
||||
android:id="@+id/action_global_authenticationFragment"
|
||||
app:destination="@id/authenticationFragment" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_global_recipesListFragment"
|
||||
app:destination="@id/recipesListFragment"
|
||||
app:popUpTo="@id/nav_graph" />
|
||||
<action
|
||||
android:id="@+id/action_global_recipesListFragment"
|
||||
app:destination="@id/recipesListFragment"
|
||||
app:popUpTo="@id/nav_graph" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_global_addRecipeFragment"
|
||||
app:destination="@id/addRecipeFragment"
|
||||
app:popUpTo="@id/recipesListFragment" />
|
||||
<action
|
||||
android:id="@+id/action_global_addRecipeFragment"
|
||||
app:destination="@id/addRecipeFragment"
|
||||
app:popUpTo="@id/recipesListFragment" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_global_baseURLFragment"
|
||||
app:destination="@id/baseURLFragment"
|
||||
app:popUpTo="@id/recipesListFragment" />
|
||||
<action
|
||||
android:id="@+id/action_global_baseURLFragment"
|
||||
app:destination="@id/baseURLFragment"
|
||||
app:popUpTo="@id/recipesListFragment" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_global_shoppingListsFragment"
|
||||
app:destination="@id/shoppingListsFragment"
|
||||
app:popUpTo="@id/recipesListFragment" />
|
||||
<action
|
||||
android:id="@+id/action_global_shoppingListsFragment"
|
||||
app:destination="@id/shoppingListsFragment"
|
||||
app:popUpTo="@id/recipesListFragment" />
|
||||
</navigation>
|
||||
Reference in New Issue
Block a user