diff --git a/data/src/main/java/com/fakedevelopers/data/di/MainModule.kt b/data/src/main/java/com/fakedevelopers/data/di/MainModule.kt index 01b02237..50ea4121 100644 --- a/data/src/main/java/com/fakedevelopers/data/di/MainModule.kt +++ b/data/src/main/java/com/fakedevelopers/data/di/MainModule.kt @@ -1,6 +1,7 @@ package com.fakedevelopers.data.di import android.content.Context +import com.fakedevelopers.data.repository.AlbumRepositoryImpl import com.fakedevelopers.data.repository.ChatRepositoryImpl import com.fakedevelopers.data.repository.ImageRepositoryImpl import com.fakedevelopers.data.repository.LocalStorageRepositoryImpl @@ -16,6 +17,7 @@ import com.fakedevelopers.data.service.ProductEditorService import com.fakedevelopers.data.service.ProductListService import com.fakedevelopers.data.service.ProductSearchService import com.fakedevelopers.data.source.LocalStorageDataSource +import com.fakedevelopers.domain.repository.AlbumRepository import com.fakedevelopers.domain.repository.ChatRepository import com.fakedevelopers.domain.repository.ImageRepository import com.fakedevelopers.domain.repository.LocalStorageRepository @@ -112,4 +114,9 @@ object MainModule { @Provides fun provideProductSearchRepository(service: ProductSearchService): ProductSearchRepository = ProductSearchRepositoryImpl(service) + + @Singleton + @Provides + fun provideAlbumRepository(): AlbumRepository = + AlbumRepositoryImpl() } diff --git a/data/src/main/java/com/fakedevelopers/data/repository/AlbumRepositoryImpl.kt b/data/src/main/java/com/fakedevelopers/data/repository/AlbumRepositoryImpl.kt new file mode 100644 index 00000000..f221d832 --- /dev/null +++ b/data/src/main/java/com/fakedevelopers/data/repository/AlbumRepositoryImpl.kt @@ -0,0 +1,38 @@ +package com.fakedevelopers.data.repository + +import com.fakedevelopers.domain.model.AlbumInfo +import com.fakedevelopers.domain.model.AlbumItem +import com.fakedevelopers.domain.repository.AlbumRepository + +class AlbumRepositoryImpl : AlbumRepository { + override fun getAlbumInfo( + albumItems: List, + allImagesTitle: String + ): List { + val albumInfo = mutableListOf() + albumInfo.add( + AlbumInfo( + path = "", + firstImage = albumItems[0].uri, + name = allImagesTitle, + count = albumItems.size + ) + ) + val countMap = mutableMapOf() + albumItems.forEach { albumItem -> + val relPath = albumItem.path.substringBeforeLast('/') + countMap[relPath] = (countMap[relPath] ?: 0) + 1 + } + countMap.keys.forEach { relPath -> + albumInfo.add( + AlbumInfo( + path = relPath, + firstImage = albumItems.find { it.path.contains(relPath) }?.uri ?: "", + name = relPath.substringAfterLast('/'), + count = countMap[relPath] ?: 0 + ) + ) + } + return albumInfo + } +} diff --git a/data/src/main/java/com/fakedevelopers/data/repository/ImageRepositoryImpl.kt b/data/src/main/java/com/fakedevelopers/data/repository/ImageRepositoryImpl.kt index 630b2d4e..3cda6023 100644 --- a/data/src/main/java/com/fakedevelopers/data/repository/ImageRepositoryImpl.kt +++ b/data/src/main/java/com/fakedevelopers/data/repository/ImageRepositoryImpl.kt @@ -2,13 +2,19 @@ package com.fakedevelopers.data.repository import android.content.ContentResolver import android.content.ContentUris +import android.database.ContentObserver import android.net.Uri +import android.os.Handler +import android.os.Looper import android.provider.MediaStore import androidx.exifinterface.media.ExifInterface import com.fakedevelopers.domain.model.AlbumItem import com.fakedevelopers.domain.model.MediaInfo import com.fakedevelopers.domain.repository.ImageRepository import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.withContext import java.util.Locale import javax.inject.Inject @@ -16,6 +22,7 @@ import javax.inject.Inject class ImageRepositoryImpl @Inject constructor( private val contentResolver: ContentResolver ) : ImageRepository { + override fun isValid(uri: String): Boolean { contentResolver.runCatching { openFileDescriptor(Uri.parse(uri), "r")?.use { @@ -25,11 +32,14 @@ class ImageRepositoryImpl @Inject constructor( return false } - override suspend fun getAllImages(): Map> { + override suspend fun getImages(path: String?): List { val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI - val albums = mutableMapOf>().apply { - this[ALL_PICTURES] = mutableListOf() + val where = if (path.isNullOrEmpty().not()) { + MediaStore.Images.Media.DATA + " LIKE '%$path%'" + } else { + null } + val images = mutableListOf() contentResolver.query( uri, arrayOf( @@ -37,7 +47,7 @@ class ImageRepositoryImpl @Inject constructor( MediaStore.Images.Media.DATA, MediaStore.Images.ImageColumns.DATE_MODIFIED ), - null, + where, null, MediaStore.Images.ImageColumns.DATE_MODIFIED + " DESC" )?.use { cursor -> @@ -49,19 +59,30 @@ class ImageRepositoryImpl @Inject constructor( ).toString() // 최근 수정 날짜 val date = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED)) - val albumItem = AlbumItem(imageUri, date) - // 전체보기에 저장 - albums[ALL_PICTURES]?.add(albumItem) - // 이미지 상대 경로에 저장 - cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)).let { path -> - val url = path.substringBeforeLast("/") - albums[url]?.add(albumItem) ?: run { - albums[url] = mutableListOf(albumItem) - } + val realPath = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)) + images.add(AlbumItem(imageUri, date, realPath)) + } + } + return images + } + + override fun getImageObserver(): Flow = callbackFlow { + val contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + if (uri != null) { + trySend(uri.toString()) } } } - return albums + contentResolver.registerContentObserver( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + true, + contentObserver + ) + awaitClose { + contentResolver.unregisterContentObserver(contentObserver) + } } override fun getValidUris(uris: List): List = @@ -138,8 +159,4 @@ class ImageRepositoryImpl @Inject constructor( } updatedAlbumItem } - - companion object { - private const val ALL_PICTURES = "전체보기" - } } diff --git a/domain/src/main/java/com/fakedevelopers/domain/model/AlbumInfo.kt b/domain/src/main/java/com/fakedevelopers/domain/model/AlbumInfo.kt new file mode 100644 index 00000000..4a77a575 --- /dev/null +++ b/domain/src/main/java/com/fakedevelopers/domain/model/AlbumInfo.kt @@ -0,0 +1,8 @@ +package com.fakedevelopers.domain.model + +data class AlbumInfo( + val path: String, + val firstImage: String, + val name: String, + val count: Int +) diff --git a/domain/src/main/java/com/fakedevelopers/domain/repository/AlbumRepository.kt b/domain/src/main/java/com/fakedevelopers/domain/repository/AlbumRepository.kt new file mode 100644 index 00000000..1d9c0b61 --- /dev/null +++ b/domain/src/main/java/com/fakedevelopers/domain/repository/AlbumRepository.kt @@ -0,0 +1,8 @@ +package com.fakedevelopers.domain.repository + +import com.fakedevelopers.domain.model.AlbumInfo +import com.fakedevelopers.domain.model.AlbumItem + +interface AlbumRepository { + fun getAlbumInfo(albumItems: List, allImagesTitle: String): List +} diff --git a/domain/src/main/java/com/fakedevelopers/domain/repository/ImageRepository.kt b/domain/src/main/java/com/fakedevelopers/domain/repository/ImageRepository.kt index 19a6ad21..658f52b1 100644 --- a/domain/src/main/java/com/fakedevelopers/domain/repository/ImageRepository.kt +++ b/domain/src/main/java/com/fakedevelopers/domain/repository/ImageRepository.kt @@ -4,10 +4,12 @@ import com.fakedevelopers.domain.model.AlbumItem import com.fakedevelopers.domain.model.MediaInfo import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow interface ImageRepository { fun isValid(uri: String): Boolean - suspend fun getAllImages(): Map> + suspend fun getImages(path: String?): List + fun getImageObserver(): Flow fun getValidUris(uris: List): List fun getDateModifiedByUri(uri: String): AlbumItem? suspend fun getBytesByUri(uri: String, dispatcher: CoroutineDispatcher = Dispatchers.IO): ByteArray? diff --git a/domain/src/main/java/com/fakedevelopers/domain/usecase/GetAlbumInfoUseCase.kt b/domain/src/main/java/com/fakedevelopers/domain/usecase/GetAlbumInfoUseCase.kt new file mode 100644 index 00000000..15598026 --- /dev/null +++ b/domain/src/main/java/com/fakedevelopers/domain/usecase/GetAlbumInfoUseCase.kt @@ -0,0 +1,15 @@ +package com.fakedevelopers.domain.usecase + +import com.fakedevelopers.domain.repository.AlbumRepository +import javax.inject.Inject + +class GetAlbumInfoUseCase @Inject constructor( + private val getImagesUseCase: GetImagesUseCase, + private val albumRepository: AlbumRepository +) { + suspend operator fun invoke(allImagesTitle: String) = + albumRepository.getAlbumInfo( + albumItems = getImagesUseCase(), + allImagesTitle = allImagesTitle + ) +} diff --git a/domain/src/main/java/com/fakedevelopers/domain/usecase/GetAllImagesUseCase.kt b/domain/src/main/java/com/fakedevelopers/domain/usecase/GetImageObserverUseCase.kt similarity index 53% rename from domain/src/main/java/com/fakedevelopers/domain/usecase/GetAllImagesUseCase.kt rename to domain/src/main/java/com/fakedevelopers/domain/usecase/GetImageObserverUseCase.kt index 53bdc7e2..92b10858 100644 --- a/domain/src/main/java/com/fakedevelopers/domain/usecase/GetAllImagesUseCase.kt +++ b/domain/src/main/java/com/fakedevelopers/domain/usecase/GetImageObserverUseCase.kt @@ -1,10 +1,11 @@ package com.fakedevelopers.domain.usecase import com.fakedevelopers.domain.repository.ImageRepository +import kotlinx.coroutines.flow.Flow import javax.inject.Inject -class GetAllImagesUseCase @Inject constructor( +class GetImageObserverUseCase @Inject constructor( private val repository: ImageRepository ) { - suspend operator fun invoke() = repository.getAllImages() + operator fun invoke(): Flow = repository.getImageObserver() } diff --git a/domain/src/main/java/com/fakedevelopers/domain/usecase/GetImagesUseCase.kt b/domain/src/main/java/com/fakedevelopers/domain/usecase/GetImagesUseCase.kt new file mode 100644 index 00000000..cc16eb61 --- /dev/null +++ b/domain/src/main/java/com/fakedevelopers/domain/usecase/GetImagesUseCase.kt @@ -0,0 +1,12 @@ +package com.fakedevelopers.domain.usecase + +import com.fakedevelopers.domain.model.AlbumItem +import com.fakedevelopers.domain.repository.ImageRepository +import javax.inject.Inject + +class GetImagesUseCase @Inject constructor( + private val repository: ImageRepository +) { + suspend operator fun invoke(path: String? = null): List = + repository.getImages(path) +} diff --git a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/ProductEditorFragment.kt b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/ProductEditorFragment.kt index 92881391..8811dd9f 100644 --- a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/ProductEditorFragment.kt +++ b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/ProductEditorFragment.kt @@ -11,7 +11,6 @@ import android.view.View import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.EditText -import androidx.activity.OnBackPressedCallback import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes @@ -72,14 +71,6 @@ abstract class ProductEditorFragment( ) } - private val backPressedCallback by lazy { - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - handleOnBackPressed() - } - } - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.vm = viewModel @@ -94,7 +85,6 @@ abstract class ProductEditorFragment( override fun onStart() { super.onStart() - requireActivity().onBackPressedDispatcher.addCallback(backPressedCallback) viewModel.refreshImages() binding.textviewProductEditorContentLength.isVisible = binding.edittextProductEditorContent.isFocused @@ -121,8 +111,6 @@ abstract class ProductEditorFragment( protected abstract fun initSelectedImages() - protected abstract fun handleOnBackPressed() - protected open fun initListener() { // 가격 필터 등록 binding.edittextProductEditorHopePrice.setPriceFilter(MAX_PRICE_LENGTH) @@ -168,7 +156,7 @@ abstract class ProductEditorFragment( } // 툴바 뒤로가기 버튼 binding.includeProductEditorToolbar.buttonToolbarBack.setOnClickListener { - backPressedCallback.handleOnBackPressed() + findNavController().popBackStack() } binding.spinnerProductEditorCategory.apply { onItemSelectedListener = object : AdapterView.OnItemSelectedListener { @@ -254,7 +242,6 @@ abstract class ProductEditorFragment( override fun onDestroyView() { super.onDestroyView() - backPressedCallback.remove() keyboardVisibilityUtils.deleteKeyboardListeners() } diff --git a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/ProductEditorViewModel.kt b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/ProductEditorViewModel.kt index 392252eb..ec6cb7ff 100644 --- a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/ProductEditorViewModel.kt +++ b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/ProductEditorViewModel.kt @@ -166,8 +166,8 @@ class ProductEditorViewModel @Inject constructor( productModificationDto: ProductModificationDto? = null ) { selectedImageInfo?.let { - selectedImageInfo.uris = it.uris - selectedImageInfo.changeBitmaps.putAll(it.changeBitmaps) + this.selectedImageInfo.uris = it.uris + this.selectedImageInfo.changeBitmaps.putAll(it.changeBitmaps) adapter.submitList(it.uris.toMutableList()) } productModificationDto?.let { diff --git a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumList/AlbumListFragment.kt b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumList/AlbumListFragment.kt index 39501c05..fb134900 100644 --- a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumList/AlbumListFragment.kt +++ b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumList/AlbumListFragment.kt @@ -1,21 +1,17 @@ package com.fakedevelopers.presentation.ui.productEditor.albumList -import android.database.ContentObserver -import android.net.Uri import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.provider.MediaStore +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.RelativeSizeSpan import android.view.View -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.activity.OnBackPressedCallback +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import com.fakedevelopers.presentation.R import com.fakedevelopers.presentation.databinding.FragmentAlbumListBinding @@ -23,6 +19,7 @@ import com.fakedevelopers.presentation.ui.base.BaseFragment import com.fakedevelopers.presentation.ui.productEditor.DragAndDropCallback import com.fakedevelopers.presentation.ui.util.repeatOnStarted import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest @AndroidEntryPoint class AlbumListFragment : BaseFragment( @@ -39,7 +36,7 @@ class AlbumListFragment : BaseFragment( if (viewModel.albumViewMode.value == AlbumViewState.PAGER) { viewModel.setAlbumViewMode(AlbumViewState.GRID) } else { - toProductRegistration(args.selectedImageInfo) + findNavController().popBackStack() } } } @@ -48,28 +45,7 @@ class AlbumListFragment : BaseFragment( object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { super.onPageSelected(position) - setPagerUI(position) - } - } - } - - // 외부 저장소에 변화가 생기면 얘가 호출이 됩니다. - private val contentObserver by lazy { - object : ContentObserver(Handler(Looper.getMainLooper())) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - super.onChange(selfChange, uri) - if (uri != null) { - viewModel.onAlbumListUpdated(uri.toString()) - } - } - } - } - - private val albumLayoutManager by lazy { - object : GridLayoutManager(requireContext(), 3) { - override fun onLayoutCompleted(state: RecyclerView.State?) { - super.onLayoutCompleted(state) - viewModel.scrollToTop() + viewModel.setCurrentViewPagerIdx(position) } } } @@ -79,18 +55,11 @@ class AlbumListFragment : BaseFragment( binding.vm = viewModel if (args.selectedImageInfo.uris.isNotEmpty()) { viewModel.initSelectedImageList(args.selectedImageInfo) - binding.buttonAlbumListComplete.visibility = View.VISIBLE - } - requireActivity().contentResolver.registerContentObserver( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - true, - contentObserver - ) - binding.recyclerAlbumList.run { - layoutManager = albumLayoutManager - itemAnimator = null + setCompleteTextVisibility(args.selectedImageInfo.uris.size) } + binding.recyclerAlbumList.itemAnimator = null initListener() + initCollector() } override fun onStart() { @@ -100,36 +69,79 @@ class AlbumListFragment : BaseFragment( viewModel.checkSelectedImages(binding.viewpagerPictureSelect.currentItem) } - private fun toProductRegistration(selectedImageInfo: SelectedImageInfo) { + private fun toProductEditor(selectedImageInfo: SelectedImageInfo) { selectedImageInfo.run { uris.clear() uris.addAll(viewModel.selectedImageInfo.uris) changeBitmaps.clear() changeBitmaps.putAll(viewModel.selectedImageInfo.changeBitmaps) } - findNavController().popBackStack() + findNavController().run { + if (backQueue.any { it.destination.id == R.id.productRegistrationFragment }) { + navigate( + AlbumListFragmentDirections + .actionPictureSelectFragmentToProductRegistrationFragment(selectedImageInfo) + ) + } else { + popBackStack() + } + } } private fun initListener() { - binding.buttonAlbumListComplete.setOnClickListener { - toProductRegistration(args.selectedImageInfo) + binding.toolbarAlbumList.run { + textviewAlbumComplete.setOnClickListener { + toProductEditor(args.selectedImageInfo) + } + textviewAlbumTitle.setOnClickListener { + if (viewModel.albumViewMode.value == AlbumViewState.GRID) { + findNavController().navigate( + AlbumListFragmentDirections.actionPictureSelectFragmentToAlbumSelectFragment( + selectedImageInfo = viewModel.selectedImageInfo, + title = binding.toolbarAlbumList.textviewAlbumTitle.text.toString().substringBeforeLast(' ') + ) + ) + } + } + buttonAlbumClose.setOnClickListener { + findNavController().popBackStack() + } } binding.viewpagerPictureSelect.registerOnPageChangeCallback(onPageChangeCallback) ItemTouchHelper(DragAndDropCallback(viewModel.selectedPictureAdapter)) .attachToRecyclerView(binding.recyclerSelectedPicture) + } + + private fun initCollector() { repeatOnStarted(viewLifecycleOwner) { viewModel.event.collect { event -> handleEvent(event) } } + repeatOnStarted(viewLifecycleOwner) { + viewModel.albumTitle.collectLatest { title -> + val toolbarTitle = getString( + R.string.album_list_title, + title.ifEmpty { getString(R.string.album_select_recent_images) } + ) + binding.toolbarAlbumList.textviewAlbumTitle.text = + SpannableStringBuilder(toolbarTitle).apply { + setSpan( + RelativeSizeSpan(0.5f), + toolbarTitle.lastIndex, + toolbarTitle.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + } } private fun handleEvent(event: AlbumListViewModel.Event) { when (event) { - is AlbumListViewModel.Event.AlbumList -> initSpinner(event.albums) + is AlbumListViewModel.Event.AlbumList -> viewModel.updateAlbumList() is AlbumListViewModel.Event.ImageCount -> handleImageCount(event.count) - is AlbumListViewModel.Event.OnListChange -> onAlbumChanged(event.state) - is AlbumListViewModel.Event.ScrollToTop -> scrollAlbumListToTop() + is AlbumListViewModel.Event.OnListChange -> setCompleteTextVisibility(event.count) is AlbumListViewModel.Event.SelectErrorImage -> sendSnackBar(getString(R.string.album_selected_error_image)) is AlbumListViewModel.Event.StartViewPagerIndex -> initViewPagerIndex(event.idx) } @@ -147,56 +159,26 @@ class AlbumListFragment : BaseFragment( } } - private fun onAlbumChanged(state: Boolean) { - binding.buttonAlbumListComplete.visibility = - if (state) { - View.INVISIBLE - } else { - View.VISIBLE - } - } - - private fun scrollAlbumListToTop() { - binding.recyclerAlbumList.run { - post { scrollToPosition(0) } - } - } - - private fun initSpinner(albums: List) { - binding.spinnerAlbumList.apply { - adapter = ArrayAdapter( - requireContext(), - android.R.layout.simple_spinner_item, - albums.map { album -> album.substringAfterLast("/") } - ) - onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - viewModel.updateAlbumList(albums[position]) - } - - override fun onNothingSelected(parent: AdapterView<*>?) { - // 안쓸거야!! - } - } + private fun setCompleteTextVisibility(count: Int) { + val state = count != 0 + val colorId = if (state) R.color.black else R.color.gray_80 + binding.toolbarAlbumList.run { + textviewAlbumCount.isVisible = state + textviewAlbumCount.text = count.toString() + textviewAlbumComplete.isEnabled = state + textviewAlbumComplete.setTextColor(ContextCompat.getColor(requireContext(), colorId)) } } private fun initViewPagerIndex(idx: Int) { if (viewModel.currentViewPagerIdx == idx) { - setPagerUI(idx) + viewModel.setCurrentViewPagerIdx(idx) } binding.viewpagerPictureSelect.setCurrentItem(idx, false) } - private fun setPagerUI(position: Int) { - // 사진 편집 대상을 알기 위해 현재 보고 있는 이미지의 인덱스 저장 - viewModel.setCurrentViewPagerIdx(position) - binding.textviewAlbumListIndex.text = viewModel.getCurrentPositionString(position + 1) - } - override fun onDestroyView() { binding.viewpagerPictureSelect.unregisterOnPageChangeCallback(onPageChangeCallback) - requireActivity().contentResolver.unregisterContentObserver(contentObserver) backPressedCallback.remove() super.onDestroyView() } diff --git a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumList/AlbumListViewModel.kt b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumList/AlbumListViewModel.kt index ac5965de..b92dfdda 100644 --- a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumList/AlbumListViewModel.kt +++ b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumList/AlbumListViewModel.kt @@ -1,10 +1,12 @@ package com.fakedevelopers.presentation.ui.productEditor.albumList +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.fakedevelopers.domain.model.AlbumItem -import com.fakedevelopers.domain.usecase.GetAllImagesUseCase import com.fakedevelopers.domain.usecase.GetDateModifiedByUriUseCase +import com.fakedevelopers.domain.usecase.GetImageObserverUseCase +import com.fakedevelopers.domain.usecase.GetImagesUseCase import com.fakedevelopers.domain.usecase.GetValidUrisUseCase import com.fakedevelopers.domain.usecase.IsValidUriUseCase import com.fakedevelopers.presentation.ui.productEditor.SelectedPictureListAdapter @@ -21,10 +23,12 @@ import kotlin.math.roundToInt @HiltViewModel class AlbumListViewModel @Inject constructor( + args: SavedStateHandle, isValidUriUseCase: IsValidUriUseCase, private val getDateModifiedFromUriUseCase: GetDateModifiedByUriUseCase, private val getValidUrisUseCase: GetValidUrisUseCase, - private val getAllImagesUseCase: GetAllImagesUseCase + private val getImagesUseCase: GetImagesUseCase, + private val getImageObserverUseCase: GetImageObserverUseCase ) : ViewModel() { private val _albumViewMode = MutableStateFlow(AlbumViewState.GRID) val albumViewMode: StateFlow get() = _albumViewMode @@ -35,31 +39,36 @@ class AlbumListViewModel @Inject constructor( private val _editButtonEnableState = MutableStateFlow(false) val editButtonEnableState: StateFlow get() = _editButtonEnableState + private val _albumTitle = MutableStateFlow("") + val albumTitle: StateFlow get() = _albumTitle + private val updatedImageList = hashSetOf() - private var currentAlbum = "" - private var totalPictureCount = 0 - private var allImages = mapOf>() + private var allImages = mutableListOf() val selectedImageInfo = SelectedImageInfo() // 현재 뷰 페이저 인덱스 var currentViewPagerIdx = 0 private set - // 앨범 전환 시 리스트를 탑으로 올리기 위한 플래그 - private var scrollToTopFlag = false + private val path = args.get("albumPath") ?: "" + val title = path.substringAfterLast('/') init { viewModelScope.launch { - allImages = getAllImagesUseCase() - sendEvent(Event.AlbumList(allImages.keys.toList())) + _albumTitle.emit(title) + allImages = getImagesUseCase(path).toMutableList() + sendEvent(Event.AlbumList(allImages)) + getImageObserverUseCase().collect { uri -> + updatedImageList.add(uri) + } } } // 그리드 앨범 리스트 어뎁터 val albumListAdapter = AlbumListAdapter( isValidUri = { uri -> isValidUriUseCase(uri) }, - findSelectedImageIndex = { findSelectedImageIndex(it) }, + findSelectedImageIndex = { selectedImageInfo.uris.indexOf(it) }, sendErrorToast = { sendEvent(Event.SelectErrorImage(true)) }, showViewPager = { uri -> showViewPager(uri) } ) { uri, state -> @@ -72,7 +81,7 @@ class AlbumListViewModel @Inject constructor( sendErrorToast = { sendEvent(Event.SelectErrorImage(true)) }, getEditedImage = { uri -> selectedImageInfo.changeBitmaps[uri] } ) { uri -> - setSelectedState(uri, findSelectedImageIndex(uri) == -1) + setSelectedState(uri, selectedImageInfo.uris.indexOf(uri) == -1) } // 선택 사진 리스트 어뎁터 @@ -81,7 +90,7 @@ class AlbumListViewModel @Inject constructor( setSelectedState(it) albumListAdapter.refreshSelectedOrder() }, - findSelectedImageIndex = { findSelectedImageIndex(it) }, + findSelectedImageIndex = { selectedImageInfo.uris.indexOf(it) }, swapSelectedImage = { fromPosition, toPosition -> swapSelectedImage(fromPosition, toPosition) } ) @@ -99,7 +108,7 @@ class AlbumListViewModel @Inject constructor( } fun rotateCurrentImage() { - val uri = getCurrentUri() + val uri = allImages[currentViewPagerIdx].uri // 로테이트된 비트맵이 있으면 그걸 돌림 // 없다면 새로 추가 selectedImageInfo.changeBitmaps[uri]?.let { bitmapInfo -> @@ -115,7 +124,7 @@ class AlbumListViewModel @Inject constructor( // 수정된 이미지 비트맵 추가 private fun addEditedBitmapInfo(uri: String) { - if (findSelectedImageIndex(uri) == -1) { + if (selectedImageInfo.uris.indexOf(uri) == -1) { setSelectedState(uri, true) } selectedImageInfo.changeBitmaps[uri] = BitmapInfo(ROTATE_DEGREE) @@ -123,13 +132,19 @@ class AlbumListViewModel @Inject constructor( fun setCurrentViewPagerIdx(idx: Int) { currentViewPagerIdx = idx - sendEvent(Event.ImageCount(findSelectedImageIndex(getCurrentUri()))) + sendEvent(Event.ImageCount(selectedImageInfo.uris.indexOf(allImages[currentViewPagerIdx].uri))) + viewModelScope.launch { + _albumTitle.emit("${idx + 1} / ${allImages.size}") + } } fun setAlbumViewMode(state: AlbumViewState) { // 보기 모드를 전환하기 전에 변경 사항을 반영해준다 if (state == AlbumViewState.GRID) { albumListAdapter.refreshAll() + viewModelScope.launch { + _albumTitle.emit(title) + } } viewModelScope.launch { _albumViewMode.emit(state) @@ -144,7 +159,7 @@ class AlbumListViewModel @Inject constructor( viewModelScope.launch { selectedPictureAdapter.submitList(list.toMutableList()) if (list.isNotEmpty() && !list.contains(selectedImageInfo.uris[0])) { - selectedPictureAdapter.notifyItemChanged(findSelectedImageIndex(list[0])) + selectedPictureAdapter.notifyItemChanged(selectedImageInfo.uris.indexOf(list[0])) } setAdapterList() } @@ -156,14 +171,12 @@ class AlbumListViewModel @Inject constructor( if (state) { selectedImageInfo.uris.add(uri) } else { - val idx = findSelectedImageIndex(uri) + val idx = selectedImageInfo.uris.indexOf(uri) selectedImageInfo.uris.removeAt(idx) // 수정된 내용(BitmapInfo)도 같이 삭제 if (selectedImageInfo.changeBitmaps.remove(uri) != null) { // 페이저에 보이는 이미지 원상 복구 - allImages[currentAlbum]?.let { list -> - albumPagerAdapter.notifyItemChanged(list.indexOfFirst { it.uri == uri }) - } + albumPagerAdapter.notifyItemChanged(allImages.indexOfFirst { it.uri == uri }) } // 첫번째 사진이 삭제 된다면 다음 사진에게 대표직을 물려줌 if (selectedImageInfo.uris.isNotEmpty() && idx == 0) { @@ -185,50 +198,26 @@ class AlbumListViewModel @Inject constructor( setSelectedImageList() } - fun scrollToTop() { - if (scrollToTopFlag && albumListAdapter.currentList[0] == allImages[currentAlbum]?.get(0)) { - sendEvent(Event.ScrollToTop(true)) - scrollToTopFlag = false - } - } - - private fun findSelectedImageIndex(uri: String) = selectedImageInfo.uris.indexOf(uri) - - fun getCurrentPositionString(position: Int) = "$position / $totalPictureCount" - - private fun getCurrentUri() = allImages[currentAlbum]?.get(currentViewPagerIdx)?.uri ?: "" - - private fun getPictureUri(albumName: String = currentAlbum, position: Int) = - allImages[albumName]?.get(position)?.uri ?: "" - - fun onAlbumListUpdated(uri: String) { - updatedImageList.add(uri) - } - // 편집 버튼 클릭 fun onEditButtonClick() { showViewPager(selectedImageInfo.uris.last()) } - fun updateAlbumList(albumName: String? = null) { + fun updateAlbumList() { val updatedAlbumItems = updatedImageList.mapNotNull { getDateModifiedFromUriUseCase(it) } updatedImageList.forEach { uri -> removeImage(uri) } // 앨범 리스트 갱신 updatedAlbumItems.forEach { item -> - val albumItem = AlbumItem(item.uri, item.modified) - allImages[ALL_PICTURES]?.add(albumItem) - allImages[item.path]?.add(albumItem) + allImages.add(AlbumItem(item.uri, item.modified)) } // 수정된 날짜 기준으로 소팅 if (updatedAlbumItems.isNotEmpty()) { - for (key in allImages.keys) { - allImages[key]?.sortByDescending { it.modified } - } + allImages.sortByDescending { it.modified } } updatedImageList.clear() - setAdapterList(albumName ?: currentAlbum) + setAdapterList() } fun checkSelectedImages(idx: Int? = null) { @@ -237,46 +226,34 @@ class AlbumListViewModel @Inject constructor( // 유효한 선택 이미지 리스트로 갱신 setSelectedImage(getValidUrisUseCase(selectedImageInfo.uris)) if (albumViewMode.value == AlbumViewState.PAGER && idx != null) { - sendEvent(Event.ImageCount(findSelectedImageIndex(getPictureUri(position = idx)))) + sendEvent(Event.ImageCount(selectedImageInfo.uris.indexOf(allImages[idx].uri))) } } } private fun removeImage(uri: String) { - val targetImage = allImages[ALL_PICTURES]?.find { it.uri == uri } ?: return - for (key in allImages.keys) { - allImages[key]?.remove(targetImage) - } + val targetImage = allImages.find { it.uri == uri } ?: return + allImages.remove(targetImage) } - private fun setAdapterList(albumName: String = currentAlbum) { - allImages[albumName]?.let { list -> - val currentList = mutableListOf().apply { addAll(list) } - albumListAdapter.submitList(currentList) - albumPagerAdapter.submitList(currentList) - totalPictureCount = list.size - } - if (albumName != currentAlbum) { - currentAlbum = albumName - // 앨범을 바꿀 때 최상위 스크롤을 해주는 플래그를 true로 바꿔준다. - scrollToTopFlag = true - } + private fun setAdapterList() { + val currentList = mutableListOf().apply { addAll(allImages) } + albumListAdapter.submitList(currentList) + albumPagerAdapter.submitList(currentList) } // 앨범 뷰 페이저 private fun showViewPager(uri: String) { - allImages[currentAlbum]?.let { album -> - val idx = album.indexOf(album.find { it.uri == uri }) - if (idx != -1) { - sendEvent(Event.StartViewPagerIndex(idx)) - setAlbumViewMode(AlbumViewState.PAGER) - } + val idx = allImages.indexOfFirst { it.uri == uri } + if (idx != -1) { + sendEvent(Event.StartViewPagerIndex(idx)) + setAlbumViewMode(AlbumViewState.PAGER) } } private fun setSelectedImageList() { selectedPictureAdapter.submitList(selectedImageInfo.uris.toMutableList()) - sendEvent(Event.OnListChange(selectedImageInfo.uris.isEmpty())) + sendEvent(Event.OnListChange(selectedImageInfo.uris.size)) } private fun swapSelectedImage(fromPosition: Int, toPosition: Int) { @@ -292,15 +269,10 @@ class AlbumListViewModel @Inject constructor( } sealed class Event { - data class OnListChange(val state: Boolean) : Event() + data class OnListChange(val count: Int) : Event() data class SelectErrorImage(val state: Boolean) : Event() data class StartViewPagerIndex(val idx: Int) : Event() data class ImageCount(val count: Int) : Event() - data class ScrollToTop(val state: Boolean) : Event() - data class AlbumList(val albums: List) : Event() - } - - companion object { - private const val ALL_PICTURES = "전체보기" + data class AlbumList(val albums: List) : Event() } } diff --git a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumSelect/AlbumSelectAdapter.kt b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumSelect/AlbumSelectAdapter.kt new file mode 100644 index 00000000..9a622562 --- /dev/null +++ b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumSelect/AlbumSelectAdapter.kt @@ -0,0 +1,53 @@ +package com.fakedevelopers.presentation.ui.productEditor.albumSelect + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.fakedevelopers.domain.model.AlbumInfo +import com.fakedevelopers.presentation.R +import com.fakedevelopers.presentation.databinding.RecyclerAlbumSelectBinding + +class AlbumSelectAdapter( + private val onClick: (String) -> Unit +) : ListAdapter(diffUtil) { + + class ViewHolder( + private val binding: RecyclerAlbumSelectBinding, + private val onClick: (String) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: AlbumInfo) { + binding.albumInfo = item + binding.root.setOnClickListener { + onClick(item.path) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = + ViewHolder( + DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.recycler_album_select, + parent, + false + ), + onClick + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + companion object { + private val diffUtil = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: AlbumInfo, newItem: AlbumInfo) = + oldItem.path == newItem.path + + override fun areContentsTheSame(oldItem: AlbumInfo, newItem: AlbumInfo) = + oldItem == newItem + } + } +} diff --git a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumSelect/AlbumSelectFragment.kt b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumSelect/AlbumSelectFragment.kt new file mode 100644 index 00000000..5dc46d5c --- /dev/null +++ b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumSelect/AlbumSelectFragment.kt @@ -0,0 +1,88 @@ +package com.fakedevelopers.presentation.ui.productEditor.albumSelect + +import android.os.Bundle +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.RelativeSizeSpan +import android.view.View +import androidx.core.content.ContextCompat +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.fakedevelopers.presentation.R +import com.fakedevelopers.presentation.databinding.FragmentAlbumSelectBinding +import com.fakedevelopers.presentation.ui.base.BaseFragment +import com.fakedevelopers.presentation.ui.util.repeatOnStarted +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest + +@AndroidEntryPoint +class AlbumSelectFragment : BaseFragment( + R.layout.fragment_album_select +) { + + private val viewModel: AlbumSelectViewModel by viewModels() + private val args: AlbumSelectFragmentArgs by navArgs() + + private val adapter: AlbumSelectAdapter by lazy { + AlbumSelectAdapter { path -> + findNavController().navigate( + AlbumSelectFragmentDirections.actionAlbumSelectFragmentToPictureSelectFragment( + albumPath = path, + selectedImageInfo = args.selectedImageInfo + ) + ) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.initAlbumInfo(getString(R.string.album_select_recent_images)) + binding.recyclerAlbumSelect.adapter = adapter + val toolbarTitle = getString(R.string.album_select_title, args.title) + binding.toolbarAlbumSelect.textviewAlbumTitle.run { + text = SpannableStringBuilder(toolbarTitle).apply { + setSpan( + RelativeSizeSpan(0.5f), + toolbarTitle.lastIndex, + toolbarTitle.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + setOnClickListener { + findNavController().popBackStack() + } + } + binding.toolbarAlbumSelect.buttonAlbumClose.setOnClickListener { + // AlbumListFragment와 동일한 로직, 추상화가 필요 + findNavController().run { + if (backQueue.any { it.destination.id == R.id.productRegistrationFragment }) { + navigate( + AlbumSelectFragmentDirections + .actionAlbumSelectFragmentToProductRegistrationFragment(args.selectedImageInfo) + ) + } else { + // 이렇게 쓰면 안됨. 반드시 수정해야함 + popBackStack() + popBackStack() + } + } + } + if (args.selectedImageInfo.uris.isNotEmpty()) { + binding.toolbarAlbumSelect.run { + textviewAlbumComplete.isEnabled = true + textviewAlbumComplete.setTextColor(ContextCompat.getColor(requireContext(), R.color.black)) + textviewAlbumCount.text = args.selectedImageInfo.uris.size.toString() + } + } + initCollector() + } + + private fun initCollector() { + repeatOnStarted(viewLifecycleOwner) { + viewModel.albumInfoEvent.collectLatest { + adapter.submitList(it) + } + } + } +} diff --git a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumSelect/AlbumSelectViewModel.kt b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumSelect/AlbumSelectViewModel.kt new file mode 100644 index 00000000..37265655 --- /dev/null +++ b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productEditor/albumSelect/AlbumSelectViewModel.kt @@ -0,0 +1,26 @@ +package com.fakedevelopers.presentation.ui.productEditor.albumSelect + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.fakedevelopers.domain.model.AlbumInfo +import com.fakedevelopers.domain.usecase.GetAlbumInfoUseCase +import com.fakedevelopers.presentation.ui.util.MutableEventFlow +import com.fakedevelopers.presentation.ui.util.asEventFlow +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AlbumSelectViewModel @Inject constructor( + private val getAlbumInfoUseCase: GetAlbumInfoUseCase +) : ViewModel() { + + private val _albumInfoEvent = MutableEventFlow>() + val albumInfoEvent = _albumInfoEvent.asEventFlow() + + fun initAlbumInfo(allImagesTitle: String) { + viewModelScope.launch { + _albumInfoEvent.emit(getAlbumInfoUseCase(allImagesTitle)) + } + } +} diff --git a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productModification/ProductModificationFragment.kt b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productModification/ProductModificationFragment.kt index 7b9e9a37..d409ebba 100644 --- a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productModification/ProductModificationFragment.kt +++ b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productModification/ProductModificationFragment.kt @@ -40,8 +40,4 @@ class ProductModificationFragment : ProductEditorFragment( .attachToRecyclerView(binding.recyclerProductEditor) } } - - override fun handleOnBackPressed() { - findNavController().navigate(R.id.action_productModificationFragment_to_productListFragment) - } } diff --git a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productRegistration/ProductRegistrationFragment.kt b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productRegistration/ProductRegistrationFragment.kt index 6a8c3742..4f86e4d6 100644 --- a/presentation/src/main/java/com/fakedevelopers/presentation/ui/productRegistration/ProductRegistrationFragment.kt +++ b/presentation/src/main/java/com/fakedevelopers/presentation/ui/productRegistration/ProductRegistrationFragment.kt @@ -63,10 +63,6 @@ class ProductRegistrationFragment : ProductEditorFragment( } } - override fun handleOnBackPressed() { - findNavController().navigate(R.id.action_productRegistrationFragment_to_productListFragment) - } - override fun onDestroyView() { viewModel.saveProductWrite() super.onDestroyView() diff --git a/presentation/src/main/res/drawable/shape_picture_select_background.xml b/presentation/src/main/res/drawable/shape_picture_select_background.xml index c7d1999b..8a12e529 100644 --- a/presentation/src/main/res/drawable/shape_picture_select_background.xml +++ b/presentation/src/main/res/drawable/shape_picture_select_background.xml @@ -1,5 +1,5 @@ - - + + diff --git a/presentation/src/main/res/drawable/shape_picture_select_empty.xml b/presentation/src/main/res/drawable/shape_picture_select_empty.xml index 9622a269..40abca4a 100644 --- a/presentation/src/main/res/drawable/shape_picture_select_empty.xml +++ b/presentation/src/main/res/drawable/shape_picture_select_empty.xml @@ -2,5 +2,5 @@ + android:color="@color/white" /> diff --git a/presentation/src/main/res/layout/fragment_album_list.xml b/presentation/src/main/res/layout/fragment_album_list.xml index 80ab871f..dcbec44f 100644 --- a/presentation/src/main/res/layout/fragment_album_list.xml +++ b/presentation/src/main/res/layout/fragment_album_list.xml @@ -15,48 +15,14 @@ android:layout_height="match_parent" android:orientation="vertical"> - - -