From 46812d082e0391044933b8299f964c458a518b78 Mon Sep 17 00:00:00 2001 From: Hogu59 Date: Wed, 23 Oct 2024 16:49:23 +0900 Subject: [PATCH] :sparkles: apply viewpager to making recipe step --- .../data/model/step/RecipeStepEntity.kt | 4 +- .../android/presentation/BindingAdapters.kt | 1 + .../core/model/RecipeStepMaking.kt | 17 +++ .../making/RecipeMakingFragment2.kt | 22 ++- .../making/RecipeMakingViewModel2.kt | 113 +++++++++----- .../making/newstep/NewStepMakingAdapter.kt | 47 ++++++ .../making/newstep/NewStepMakingFragment.kt | 107 +++++++++++++ .../making/newstep/NewStepMakingViewHolder.kt | 41 +++++ .../making/newstep/NewStepMakingViewModel.kt | 143 ++++++++++++++++++ .../newstep/NewStepMakingViewModelFactory.kt | 5 + .../newstep/OnStepDataChangeListener.kt | 18 +++ .../making/step/StepMakingFragment.kt | 9 +- .../main/res/drawable/bg_photo_with_plus.xml | 26 ++++ .../app/src/main/res/drawable/bg_radius.xml | 6 + .../res/drawable/bg_radius_tiny_filled.xml | 2 +- .../res/layout/fragment_new_step_making.xml | 44 ++++++ .../src/main/res/layout/item_add_image.xml | 13 +- .../res/layout/item_minute_second_picker.xml | 2 - .../src/main/res/layout/item_step_making.xml | 71 +++++++++ .../res/layout/item_time_amount_picker.xml | 3 - .../app/src/main/res/navigation/nav_graph.xml | 8 +- 21 files changed, 626 insertions(+), 76 deletions(-) create mode 100644 android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingAdapter.kt create mode 100644 android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingFragment.kt create mode 100644 android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingViewHolder.kt create mode 100644 android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingViewModel.kt create mode 100644 android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingViewModelFactory.kt create mode 100644 android/app/src/main/java/net/pengcook/android/presentation/making/newstep/OnStepDataChangeListener.kt create mode 100644 android/app/src/main/res/drawable/bg_photo_with_plus.xml create mode 100644 android/app/src/main/res/drawable/bg_radius.xml create mode 100644 android/app/src/main/res/layout/fragment_new_step_making.xml create mode 100644 android/app/src/main/res/layout/item_step_making.xml diff --git a/android/app/src/main/java/net/pengcook/android/data/model/step/RecipeStepEntity.kt b/android/app/src/main/java/net/pengcook/android/data/model/step/RecipeStepEntity.kt index 220290b6..cecccb58 100644 --- a/android/app/src/main/java/net/pengcook/android/data/model/step/RecipeStepEntity.kt +++ b/android/app/src/main/java/net/pengcook/android/data/model/step/RecipeStepEntity.kt @@ -30,12 +30,12 @@ import net.pengcook.android.data.model.makingrecipe.entity.RecipeDescriptionEnti ], ) data class RecipeStepEntity( - @ColumnInfo(RecipeStepContract.COLUMN_ID) val id: Long = System.currentTimeMillis(), + @ColumnInfo(RecipeStepContract.COLUMN_STEP_NUMBER) val stepNumber: Int, + @ColumnInfo(RecipeStepContract.COLUMN_ID) val id: Long = (System.currentTimeMillis().toString() + stepNumber.toString()).toLong(), @ColumnInfo(RecipeStepContract.COLUMN_RECIPE_DESCRIPTION_ID) val recipeDescriptionId: Long, @ColumnInfo(RecipeStepContract.COLUMN_IMAGE_URI) val imageUri: String?, @ColumnInfo(RecipeStepContract.COLUMN_IMAGE_TITLE) val imageTitle: String?, @ColumnInfo(RecipeStepContract.COLUMN_COOKING_TIME) val cookingTime: String?, - @ColumnInfo(RecipeStepContract.COLUMN_STEP_NUMBER) val stepNumber: Int, @ColumnInfo(RecipeStepContract.COLUMN_DESCRIPTION) val description: String?, @ColumnInfo(RecipeStepContract.COLUMN_IMAGE_UPLOADED) val imageUploaded: Boolean, ) diff --git a/android/app/src/main/java/net/pengcook/android/presentation/BindingAdapters.kt b/android/app/src/main/java/net/pengcook/android/presentation/BindingAdapters.kt index a94a0045..6088f3b4 100644 --- a/android/app/src/main/java/net/pengcook/android/presentation/BindingAdapters.kt +++ b/android/app/src/main/java/net/pengcook/android/presentation/BindingAdapters.kt @@ -34,6 +34,7 @@ fun loadImage( view: ImageView, uri: Uri?, ) { + println("uri: $uri") Glide .with(view.context) .load(uri) diff --git a/android/app/src/main/java/net/pengcook/android/presentation/core/model/RecipeStepMaking.kt b/android/app/src/main/java/net/pengcook/android/presentation/core/model/RecipeStepMaking.kt index 771b1176..cff8eece 100644 --- a/android/app/src/main/java/net/pengcook/android/presentation/core/model/RecipeStepMaking.kt +++ b/android/app/src/main/java/net/pengcook/android/presentation/core/model/RecipeStepMaking.kt @@ -15,4 +15,21 @@ data class RecipeStepMaking( "cookingTime must be in the format of HH:MM:SS" } } + + val minute: String = if (cookingTime.split(":")[1] == "00") "" else cookingTime.split(":")[1] + val second: String = if (cookingTime.split(":")[2] == "00") "" else cookingTime.split(":")[2] + + companion object { + val EMPTY = + RecipeStepMaking( + stepId = 0, + recipeId = 0, + description = "", + image = "", + sequence = 0, + imageUri = "", + cookingTime = "00:00:00", + imageUploaded = false, + ) + } } diff --git a/android/app/src/main/java/net/pengcook/android/presentation/making/RecipeMakingFragment2.kt b/android/app/src/main/java/net/pengcook/android/presentation/making/RecipeMakingFragment2.kt index ab92d17f..a9ff83a4 100644 --- a/android/app/src/main/java/net/pengcook/android/presentation/making/RecipeMakingFragment2.kt +++ b/android/app/src/main/java/net/pengcook/android/presentation/making/RecipeMakingFragment2.kt @@ -4,6 +4,7 @@ import android.Manifest import android.app.AlertDialog import android.net.Uri import android.os.Bundle +import android.text.InputFilter.LengthFilter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -163,6 +164,16 @@ class RecipeMakingFragment2 : Fragment() { observeStepItems() } +/* override fun onResume() { + super.onResume() + viewModel.initRecipeSteps() + }*/ + + override fun onStart() { + super.onStart() + viewModel.initRecipeSteps() + } + override fun onDestroyView() { super.onDestroyView() _binding = null @@ -183,8 +194,8 @@ class RecipeMakingFragment2 : Fragment() { val etHour = binding.itemTimeRequired.etTimeAmountPicker.etHour val etMinute = binding.itemTimeRequired.etTimeAmountPicker.etMinute val etSecond = binding.itemTimeRequired.etTimeAmountPicker.etSecond - etHour.filters = arrayOf(MinMaxInputFilter(0, 23)) - arrayOf(MinMaxInputFilter(0, 59)).also { filters -> + etHour.filters = arrayOf(MinMaxInputFilter(0, 23), LengthFilter(2)) + arrayOf(MinMaxInputFilter(0, 59), LengthFilter(2)).also { filters -> etMinute.filters = filters etSecond.filters = filters } @@ -193,9 +204,6 @@ class RecipeMakingFragment2 : Fragment() { private fun observeStepItems() { viewModel.currentStepImages.observe(viewLifecycleOwner) { stepImageAdapter.submitList(it) - stepImageAdapter.currentList.forEach { - println("sequence : ${it.sequence}") - } } } @@ -226,9 +234,9 @@ class RecipeMakingFragment2 : Fragment() { is RecipeMakingEvent2.RecipePostFailure -> showSnackBar(getString(R.string.making_warning_post_failure)) is RecipeMakingEvent2.RecipePostSuccessful -> findNavController().navigateUp() is RecipeMakingEvent2.NavigateToMakingStep -> { - val sequence = newEvent.sequence + val sequence: Int = newEvent.sequence val action = - RecipeMakingFragment2Directions.actionRecipeMakingFragmentToStepMakingFragment(1L) + RecipeMakingFragment2Directions.actionRecipeMakingFragmentToStepMakingFragment(sequence) findNavController().navigate(action) } } diff --git a/android/app/src/main/java/net/pengcook/android/presentation/making/RecipeMakingViewModel2.kt b/android/app/src/main/java/net/pengcook/android/presentation/making/RecipeMakingViewModel2.kt index 6794d9ce..d906caf6 100644 --- a/android/app/src/main/java/net/pengcook/android/presentation/making/RecipeMakingViewModel2.kt +++ b/android/app/src/main/java/net/pengcook/android/presentation/making/RecipeMakingViewModel2.kt @@ -75,12 +75,12 @@ class RecipeMakingViewModel2 init { initRecipeDescription() - initRecipeSteps() } private fun initRecipeDescription() { viewModelScope.launch { - makingRecipeRepository.fetchRecipeDescription() + makingRecipeRepository + .fetchRecipeDescription() .onSuccess { existingRecipe -> existingRecipe?.let { recipeId = it.recipeDescriptionId @@ -94,11 +94,13 @@ class RecipeMakingViewModel2 } } - private fun initRecipeSteps() { + fun initRecipeSteps() { viewModelScope.launch { - stepMakingRepository.fetchRecipeSteps() + stepMakingRepository + .fetchRecipeSteps() .onSuccess { steps -> _currentStepImages.value = steps?.map { it.toRecipeStepImage() } ?: emptyList() + println("currentStepImages: ${currentStepImages.value}") }.onFailure { _currentStepImages.value = emptyList() } @@ -114,6 +116,8 @@ class RecipeMakingViewModel2 isLoading = false, file = File(imageUri), sequence = sequence, + description = description, + cookingTime = cookingTime, ) private fun updateSingleStepImage( @@ -165,7 +169,7 @@ class RecipeMakingViewModel2 thumbnailFile: File, ) { _thumbnailUri.value = uri - uploadSingleImage(thumbnailFile) + uploadSingleThumbnailImage(thumbnailFile) } fun changeCurrentStepImage( @@ -184,14 +188,14 @@ class RecipeMakingViewModel2 } } - private fun uploadSingleImage(file: File) { + private fun uploadSingleThumbnailImage(file: File) { viewModelScope.launch(coroutineExceptionHandler) { val presignedUrl = makingRecipeRepository.fetchImageUri(file.name) - uploadImageToS3(presignedUrl, file) + uploadThumbnailImageToS3(presignedUrl, file) } } - private suspend fun uploadImageToS3( + private suspend fun uploadThumbnailImageToS3( presignedUrl: String, file: File, ) { @@ -199,7 +203,17 @@ class RecipeMakingViewModel2 makingRecipeRepository.uploadImageToS3(presignedUrl, file) }.onSuccess { thumbnailTitle = file.name - _uiEvent.value = Event(RecipeMakingEvent2.PostImageSuccessful) + }.onFailure { + _uiEvent.value = Event(RecipeMakingEvent2.PostImageFailure) + } + } + + private suspend fun uploadImageToS3( + presignedUrl: String, + file: File, + ) { + runCatching { + makingRecipeRepository.uploadImageToS3(presignedUrl, file) }.onFailure { _uiEvent.value = Event(RecipeMakingEvent2.PostImageFailure) } @@ -226,10 +240,14 @@ class RecipeMakingViewModel2 } private fun uploadStepImages() { - viewModelScope.launch(coroutineExceptionHandler) { - currentStepImages.value?.forEach { stepImage -> + currentStepImages.value?.forEach { stepImage -> + viewModelScope.launch(coroutineExceptionHandler) { if (!stepImage.uploaded) { + println("uploaded : ${stepImage.uploaded}") uploadStepImage(stepImage) + recipeId?.let { id -> + saveRecipeSteps(id) + } } } } @@ -262,6 +280,9 @@ class RecipeMakingViewModel2 override fun onConfirm() { viewModelScope.launch { + saveRecipeSteps(recipeId ?: return@launch) + saveRecipeDescription() + if (!validateDescriptionForm()) { _uiEvent.value = Event(RecipeMakingEvent2.DescriptionFormNotCompleted) _isMakingStepButtonClicked.value = true @@ -292,7 +313,8 @@ class RecipeMakingViewModel2 return@launch } - makingRecipeRepository.postNewRecipe(recipeCreation) + makingRecipeRepository + .postNewRecipe(recipeCreation) .onSuccess { _isLoading.value = false recipeId?.let { @@ -307,8 +329,8 @@ class RecipeMakingViewModel2 } } - private fun validateDescriptionForm(): Boolean { - return !categoryContent.value.isNullOrBlank() && + private fun validateDescriptionForm(): Boolean = + !categoryContent.value.isNullOrBlank() && !introductionContent.value.isNullOrBlank() && difficultySelectedValue.value != null && !ingredientContent.value.isNullOrBlank() && @@ -317,10 +339,9 @@ class RecipeMakingViewModel2 hourContent.value != null && minuteContent.value != null && secondContent.value != null - } - private suspend fun recipeCreation(): RecipeCreation? { - return makingRecipeRepository.fetchTotalRecipeData().getOrNull()?.let { recipeData -> + private suspend fun recipeCreation(): RecipeCreation? = + makingRecipeRepository.fetchTotalRecipeData().getOrNull()?.let { recipeData -> RecipeCreation( title = recipeData.title, thumbnail = recipeData.thumbnail, @@ -332,7 +353,6 @@ class RecipeMakingViewModel2 introduction = recipeData.introduction, ) } - } private fun restoreDescriptionContents(existingRecipe: RecipeDescription) { with(existingRecipe) { @@ -341,9 +361,9 @@ class RecipeMakingViewModel2 difficultySelectedValue.value = difficulty.toFloat() / 2 introductionContent.value = description val timeParts = cookingTime.split(SEPARATOR_TIME) - hourContent.value = timeParts[0] - minuteContent.value = timeParts[1] - secondContent.value = timeParts[2] + hourContent.value = if (timeParts[0] == "00") "" else timeParts[0] + minuteContent.value = if (timeParts[1] == "00") "" else timeParts[1] + secondContent.value = if (timeParts[2] == "00") "" else timeParts[2] thumbnailTitle = thumbnail categoryContent.value = categories.joinToString() _thumbnailUri.value = Uri.parse(imageUri) @@ -365,49 +385,52 @@ class RecipeMakingViewModel2 RecipeDescription( recipeDescriptionId = recipeId ?: return, categories = - categoryContent.value?.split(SEPARATOR_INGREDIENTS) + categoryContent.value + ?.split(SEPARATOR_INGREDIENTS) ?.filter { it.trim().isNotEmpty() || it.trim().isNotBlank() } ?: emptyList(), cookingTime = formatTimeRequired(), description = introductionContent.value ?: "", difficulty = (difficultySelectedValue.value?.times(2))?.toInt() ?: 0, ingredients = - ingredientContent.value?.split(SEPARATOR_INGREDIENTS) + ingredientContent.value + ?.split(SEPARATOR_INGREDIENTS) ?.filter { it.trim().isNotEmpty() || it.trim().isNotBlank() } ?: emptyList(), thumbnail = thumbnailTitle ?: "", title = titleContent.value ?: "", imageUri = thumbnailUri.value.toString(), ) - makingRecipeRepository.saveRecipeDescription(recipeDescription) + makingRecipeRepository + .saveRecipeDescription(recipeDescription) .onSuccess { _uiEvent.value = Event(RecipeMakingEvent2.RecipeSavingSuccessful) } .onFailure { _uiEvent.value = Event(RecipeMakingEvent2.RecipeSavingFailure) } } private suspend fun saveRecipeSteps(recipeId: Long) { currentStepImages.value?.forEachIndexed { index, image -> - stepMakingRepository.saveRecipeStep( - recipeId, - RecipeStepMaking( - 1L, + stepMakingRepository + .saveRecipeStep( recipeId, - image.description, - image.imageTitle, - index + 1, - image.uri.toString(), - image.cookingTime, - imageUploaded = image.uploaded, - ), - ) + RecipeStepMaking( + 1L, + recipeId, + image.description, + image.imageTitle, + index + 1, + image.uri.toString(), + image.cookingTime, + imageUploaded = image.uploaded, + ), + ) } } - private fun formatTimeRequired(): String { - return FORMAT_TIME_REQUIRED.format( + private fun formatTimeRequired(): String = + FORMAT_TIME_REQUIRED.format( hourContent.value?.toIntOrNull() ?: 0, minuteContent.value?.toIntOrNull() ?: 0, secondContent.value?.toIntOrNull() ?: 0, ) - } override fun onAddImage() { _uiEvent.value = Event(RecipeMakingEvent2.AddThumbnailImage) @@ -423,7 +446,14 @@ class RecipeMakingViewModel2 override fun onDelete(id: Int) { _currentStepImages.value = currentStepImages.value?.filter { it.itemId != id } - _uiEvent.value = Event(RecipeMakingEvent2.ImageDeletionSuccessful(id)) + + viewModelScope.launch { + recipeId?.let { recipeId -> + stepMakingRepository.deleteRecipeStepsNonAsync(recipeId) + saveRecipeSteps(recipeId) + _uiEvent.value = Event(RecipeMakingEvent2.ImageDeletionSuccessful(id)) + } + } } override fun onOrderChange(items: List) { @@ -434,12 +464,13 @@ class RecipeMakingViewModel2 viewModelScope.launch(coroutineExceptionHandler) { val recipeId = recipeId if (recipeId != null) { + println("recipeId : $recipeId") saveRecipeSteps(recipeId) } _uiEvent.value = Event( RecipeMakingEvent2.NavigateToMakingStep( - sequence = currentStepImages.value?.indexOf(item) ?: return@launch, + sequence = currentStepImages.value?.indexOf(item)?.plus(1) ?: return@launch, ), ) } diff --git a/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingAdapter.kt b/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingAdapter.kt new file mode 100644 index 00000000..f80e11b0 --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingAdapter.kt @@ -0,0 +1,47 @@ +package net.pengcook.android.presentation.making.newstep + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import net.pengcook.android.databinding.ItemStepMakingBinding +import net.pengcook.android.presentation.core.model.RecipeStepMaking + +class NewStepMakingAdapter( + private val onStepDataChangeListener: OnStepDataChangeListener, +) : ListAdapter(diffUtil) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): NewStepMakingViewHolder { + val binding = + ItemStepMakingBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false, + ) + return NewStepMakingViewHolder(binding, onStepDataChangeListener) + } + + override fun onBindViewHolder( + holder: NewStepMakingViewHolder, + position: Int, + ) { + holder.bind(currentList[position]) + } + + companion object { + val diffUtil = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: RecipeStepMaking, + newItem: RecipeStepMaking, + ): Boolean = oldItem.stepId == newItem.stepId + + override fun areContentsTheSame( + oldItem: RecipeStepMaking, + newItem: RecipeStepMaking, + ): Boolean = oldItem == newItem + } + } +} diff --git a/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingFragment.kt b/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingFragment.kt new file mode 100644 index 00000000..166b5088 --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingFragment.kt @@ -0,0 +1,107 @@ +package net.pengcook.android.presentation.making.newstep + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import androidx.viewpager2.widget.ViewPager2 +import dagger.hilt.android.AndroidEntryPoint +import net.pengcook.android.databinding.FragmentNewStepMakingBinding + +@AndroidEntryPoint +class NewStepMakingFragment : Fragment() { + private var _binding: FragmentNewStepMakingBinding? = null + private val binding: FragmentNewStepMakingBinding + get() = _binding!! + + private val args by navArgs() + + private val viewModel: NewStepMakingViewModel by viewModels() + + private val newStepMakingAdapter: NewStepMakingAdapter by lazy { NewStepMakingAdapter(viewModel) } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentNewStepMakingBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + + initBinding() + observeViewModel() + viewModel.fetchRecipeSteps() + binding.vpStepMaking.apply { + adapter = newStepMakingAdapter + orientation = ViewPager2.ORIENTATION_HORIZONTAL + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun initBinding() { + binding.lifecycleOwner = viewLifecycleOwner + binding.vm = viewModel + binding.appbarEventListener = viewModel + } + + override fun onAttach(context: Context) { + super.onAttach(context) + + val callback: OnBackPressedCallback = + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + viewModel.exit() + } + } + + requireActivity().onBackPressedDispatcher.addCallback(this, callback) + } + + private fun observeViewModel() { + viewModel.newStepMakingEvent.observe(viewLifecycleOwner) { event -> + when (event) { + NewStepMakingEvent.NavigationEvent -> { + viewModel.exit() + } + + NewStepMakingEvent.ExitEvent -> { + // 나가기 + findNavController().navigateUp() + } + + NewStepMakingEvent.TempSaveEvent -> { + // 임시저장 + viewModel.saveData() + Toast.makeText(requireContext(), "임시저장되었습니다.", Toast.LENGTH_SHORT).show() + } + + NewStepMakingEvent.OnFetchComplete -> { + // 데이터 로딩 완료 + binding.vpStepMaking.setCurrentItem(args.sequence - 1, false) + } + } + } + + viewModel.steps.observe(viewLifecycleOwner) { steps -> + newStepMakingAdapter.submitList(steps) + } + } +} diff --git a/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingViewHolder.kt b/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingViewHolder.kt new file mode 100644 index 00000000..15b26fce --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingViewHolder.kt @@ -0,0 +1,41 @@ +package net.pengcook.android.presentation.making.newstep + +import android.text.InputFilter.LengthFilter +import androidx.core.widget.addTextChangedListener +import androidx.recyclerview.widget.RecyclerView +import net.pengcook.android.databinding.ItemStepMakingBinding +import net.pengcook.android.presentation.core.model.RecipeStepMaking +import net.pengcook.android.presentation.core.util.MinMaxInputFilter + +class NewStepMakingViewHolder( + private val binding: ItemStepMakingBinding, + private val onStepDataChangeListener: OnStepDataChangeListener, +) : RecyclerView.ViewHolder(binding.root) { + init { + val etMinute = binding.etTimeAmount.etTimeAmountPicker.etMinute + val etSecond = binding.etTimeAmount.etTimeAmountPicker.etSecond + arrayOf(MinMaxInputFilter(0, 59), LengthFilter(2)).also { filters -> + etMinute.filters = filters + etSecond.filters = filters + } + } + + fun bind(recipeStepMaking: RecipeStepMaking) { + binding.recipeStepMaking = recipeStepMaking + + binding.etTimeAmount.etTimeAmountPicker.etSecond.addTextChangedListener { + onStepDataChangeListener.onSecondChanged(recipeStepMaking.sequence - 1, it.toString()) + } + + binding.etTimeAmount.etTimeAmountPicker.etMinute.addTextChangedListener { + onStepDataChangeListener.onMinuteChanged(recipeStepMaking.sequence - 1, it.toString()) + } + + binding.etDescStepRecipe.etContent.etFormTextContent.etDefault.addTextChangedListener { + onStepDataChangeListener.onDescriptionChanged( + recipeStepMaking.sequence - 1, + it.toString(), + ) + } + } +} diff --git a/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingViewModel.kt b/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingViewModel.kt new file mode 100644 index 00000000..bad3753d --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingViewModel.kt @@ -0,0 +1,143 @@ +package net.pengcook.android.presentation.making.newstep + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import net.pengcook.android.data.repository.making.step.RecipeStepMakingRepository +import net.pengcook.android.presentation.core.listener.AppbarDoubleActionEventListener +import net.pengcook.android.presentation.core.model.RecipeStepMaking +import javax.inject.Inject + +@HiltViewModel +class NewStepMakingViewModel + @Inject + constructor( + private val recipeStepMakingRepository: RecipeStepMakingRepository, + ) : ViewModel(), + AppbarDoubleActionEventListener, + OnStepDataChangeListener { + private var _recipeId: Long = 0L + val recipeId: Long + get() = _recipeId + + private val _steps: MutableLiveData> = MutableLiveData() + val steps: LiveData> + get() = _steps + + private val _newStepMakingEvent = MutableLiveData() + val newStepMakingEvent: LiveData + get() = _newStepMakingEvent + + fun fetchRecipeSteps() { + viewModelScope.launch { + val response = recipeStepMakingRepository.fetchRecipeSteps() + response.onSuccess { recipeSteps -> + println(recipeSteps) + _steps.value = (recipeSteps ?: emptyList()) as MutableList? + _recipeId = recipeSteps?.get(0)?.recipeId ?: 0L + println("recipeId on new : $recipeId") + _newStepMakingEvent.value = NewStepMakingEvent.OnFetchComplete + } + } + } + + fun saveData() { + viewModelScope.launch { + saveRecipeSteps(recipeId) + } + } + + fun exit() { + viewModelScope.launch { + saveRecipeSteps(recipeId) + _newStepMakingEvent.value = NewStepMakingEvent.ExitEvent + } + } + + private suspend fun saveRecipeSteps(recipeId: Long) { + steps.value?.forEachIndexed { index, _ -> + recipeStepMakingRepository.saveRecipeStep( + recipeId, + steps.value!![index], + ) + } + } + + override fun navigationAction() { + _newStepMakingEvent.value = NewStepMakingEvent.NavigationEvent + } + + override fun customAction() { + _newStepMakingEvent.value = NewStepMakingEvent.TempSaveEvent + viewModelScope.launch { + saveRecipeSteps(steps.value!![0].recipeId) + } + } + + override fun onDescriptionChanged( + sequence: Int, + description: String, + ) { + val changedValue = + _steps.value?.get(sequence)?.copy(description = description) ?: RecipeStepMaking.EMPTY + _steps.value?.set(sequence, changedValue) + } + + override fun onMinuteChanged( + sequence: Int, + minute: String, + ) { + val currentMinute = if (minute.isEmpty()) 0 else minute.toInt() + val currentMinuteString = String.format(TIME_FORMAT, currentMinute) + val currentTime = _steps.value?.get(sequence)?.cookingTime ?: "00:00:00" + val currentHour = currentTime.split(":")[0] + val currentSecond = currentTime.split(":")[2] + val changedCookingTime = "$currentHour:$currentMinuteString:$currentSecond" + val changedValue = + _steps.value?.get(sequence)?.copy(cookingTime = changedCookingTime) + ?: RecipeStepMaking.EMPTY + _steps.value?.set(sequence, changedValue) + } + + override fun onSecondChanged( + sequence: Int, + second: String, + ) { + val currentSecond = if (second.isEmpty()) 0 else second.toInt() + val currentSecondString = String.format(TIME_FORMAT, currentSecond) + val currentTime = _steps.value?.get(sequence)?.cookingTime ?: "00:00:00" + val currentHour = currentTime.split(":")[0] + val currentMinute = currentTime.split(":")[1] + val changedCookingTime = "$currentHour:$currentMinute:$currentSecondString" + val changedValue = + _steps.value?.get(sequence)?.copy(cookingTime = changedCookingTime) + ?: RecipeStepMaking.EMPTY + _steps.value?.set(sequence, changedValue) + } + + companion object { + fun provideFactory(assistedFactory: NewStepMakingViewModelFactory): ViewModelProvider.Factory = + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return assistedFactory.create() as T + } + } + + private const val TIME_FORMAT = "%02d" + } + } + +sealed interface NewStepMakingEvent { + data object NavigationEvent : NewStepMakingEvent + + data object TempSaveEvent : NewStepMakingEvent + + data object OnFetchComplete : NewStepMakingEvent + + data object ExitEvent : NewStepMakingEvent +} diff --git a/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingViewModelFactory.kt b/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingViewModelFactory.kt new file mode 100644 index 00000000..74e50d28 --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/NewStepMakingViewModelFactory.kt @@ -0,0 +1,5 @@ +package net.pengcook.android.presentation.making.newstep + +interface NewStepMakingViewModelFactory { + fun create(): NewStepMakingViewModel +} diff --git a/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/OnStepDataChangeListener.kt b/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/OnStepDataChangeListener.kt new file mode 100644 index 00000000..77bcdb9c --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/presentation/making/newstep/OnStepDataChangeListener.kt @@ -0,0 +1,18 @@ +package net.pengcook.android.presentation.making.newstep + +interface OnStepDataChangeListener { + fun onDescriptionChanged( + sequence: Int, + description: String, + ) + + fun onMinuteChanged( + sequence: Int, + minute: String, + ) + + fun onSecondChanged( + sequence: Int, + second: String, + ) +} diff --git a/android/app/src/main/java/net/pengcook/android/presentation/making/step/StepMakingFragment.kt b/android/app/src/main/java/net/pengcook/android/presentation/making/step/StepMakingFragment.kt index bfdb0bdf..ffac6a0e 100644 --- a/android/app/src/main/java/net/pengcook/android/presentation/making/step/StepMakingFragment.kt +++ b/android/app/src/main/java/net/pengcook/android/presentation/making/step/StepMakingFragment.kt @@ -25,6 +25,7 @@ import net.pengcook.android.presentation.core.util.AnalyticsLogging import net.pengcook.android.presentation.core.util.FileUtils import net.pengcook.android.presentation.core.util.ImageUtils import net.pengcook.android.presentation.core.util.MinMaxInputFilter +import net.pengcook.android.presentation.making.newstep.NewStepMakingFragmentArgs import java.io.File import javax.inject.Inject @@ -33,7 +34,7 @@ class StepMakingFragment : Fragment() { private var _binding: FragmentMakingStepBinding? = null private val binding: FragmentMakingStepBinding get() = _binding!! - private val args: StepMakingFragmentArgs by navArgs() + private val args: NewStepMakingFragmentArgs by navArgs() @Inject lateinit var viewModelFactory: StepMakingViewModelFactory @@ -41,7 +42,7 @@ class StepMakingFragment : Fragment() { private val viewModel: StepMakingViewModel by viewModels { StepMakingViewModel.provideFactory( assistedFactory = viewModelFactory, - recipeId = args.recipeId, + recipeId = args.sequence.toLong(), ) } @@ -151,9 +152,9 @@ class StepMakingFragment : Fragment() { is RecipeStepMakingEvent.ImageNotUploaded -> showToast("Image is being uploaded.") is RecipeStepMakingEvent.NavigateBackToDescription -> findNavController().navigateUp() is RecipeStepMakingEvent.RecipePostSuccessful -> { - val action = + /*val action = StepMakingFragmentDirections.actionStepMakingFragmentToHomeFragment() - findNavController().navigate(action) + findNavController().navigate(action)*/ } } } diff --git a/android/app/src/main/res/drawable/bg_photo_with_plus.xml b/android/app/src/main/res/drawable/bg_photo_with_plus.xml new file mode 100644 index 00000000..23858a16 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_photo_with_plus.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/bg_radius.xml b/android/app/src/main/res/drawable/bg_radius.xml new file mode 100644 index 00000000..aa5d4dc2 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_radius.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/bg_radius_tiny_filled.xml b/android/app/src/main/res/drawable/bg_radius_tiny_filled.xml index c8f18c97..84b5068a 100644 --- a/android/app/src/main/res/drawable/bg_radius_tiny_filled.xml +++ b/android/app/src/main/res/drawable/bg_radius_tiny_filled.xml @@ -1,6 +1,6 @@ - + diff --git a/android/app/src/main/res/layout/fragment_new_step_making.xml b/android/app/src/main/res/layout/fragment_new_step_making.xml new file mode 100644 index 00000000..80b742db --- /dev/null +++ b/android/app/src/main/res/layout/fragment_new_step_making.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/item_add_image.xml b/android/app/src/main/res/layout/item_add_image.xml index 21882805..b5315467 100644 --- a/android/app/src/main/res/layout/item_add_image.xml +++ b/android/app/src/main/res/layout/item_add_image.xml @@ -28,7 +28,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:adjustViewBounds="true" - android:background="@drawable/bg_radius_tiny_filled" + android:background="@drawable/bg_photo_with_plus" android:scaleType="centerCrop" app:layout_constraintDimensionRatio="H,1:1" app:layout_constraintEnd_toEndOf="parent" @@ -36,16 +36,5 @@ app:layout_constraintTop_toTopOf="parent" bind:imageUri="@{imageUri}" /> - - diff --git a/android/app/src/main/res/layout/item_minute_second_picker.xml b/android/app/src/main/res/layout/item_minute_second_picker.xml index c2f169b0..9db8080b 100644 --- a/android/app/src/main/res/layout/item_minute_second_picker.xml +++ b/android/app/src/main/res/layout/item_minute_second_picker.xml @@ -45,7 +45,6 @@ android:importantForAutofill="no" android:inputType="numberDecimal" android:maxLength="2" - android:maxLines="1" android:padding="16dp" android:text="@={minute}" app:layout_constraintEnd_toStartOf="@id/et_second" @@ -65,7 +64,6 @@ android:importantForAutofill="no" android:inputType="numberDecimal" android:maxLength="2" - android:maxLines="1" android:padding="16dp" android:text="@={second}" app:layout_constraintEnd_toEndOf="parent" diff --git a/android/app/src/main/res/layout/item_step_making.xml b/android/app/src/main/res/layout/item_step_making.xml new file mode 100644 index 00000000..894a47e1 --- /dev/null +++ b/android/app/src/main/res/layout/item_step_making.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/item_time_amount_picker.xml b/android/app/src/main/res/layout/item_time_amount_picker.xml index ae4bc0f7..300dc426 100644 --- a/android/app/src/main/res/layout/item_time_amount_picker.xml +++ b/android/app/src/main/res/layout/item_time_amount_picker.xml @@ -48,7 +48,6 @@ android:importantForAutofill="no" android:inputType="numberDecimal" android:maxLength="2" - android:maxLines="1" android:padding="16dp" android:text="@={hour}" app:layout_constraintEnd_toStartOf="@id/et_minute" @@ -68,7 +67,6 @@ android:importantForAutofill="no" android:inputType="numberDecimal" android:maxLength="2" - android:maxLines="1" android:padding="16dp" android:text="@={minute}" app:layout_constraintEnd_toStartOf="@id/et_second" @@ -87,7 +85,6 @@ android:importantForAutofill="no" android:inputType="numberDecimal" android:maxLength="2" - android:maxLines="1" android:padding="16dp" android:text="@={second}" app:layout_constraintEnd_toEndOf="parent" diff --git a/android/app/src/main/res/navigation/nav_graph.xml b/android/app/src/main/res/navigation/nav_graph.xml index dece54aa..d03137d5 100644 --- a/android/app/src/main/res/navigation/nav_graph.xml +++ b/android/app/src/main/res/navigation/nav_graph.xml @@ -142,11 +142,11 @@ + android:name="net.pengcook.android.presentation.making.newstep.NewStepMakingFragment" + android:label="NewStepMakingFragment"> + android:name="sequence" + app:argType="integer" />