Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ sealed interface MainSideEffect {
data class NavigateToUploadAlbumWithGallery(val uriStrings: List<String>) : MainSideEffect
data class NavigateToUploadAlbumWithQRScan(val imageUrl: String) : MainSideEffect
data class ShowToast(val message: String) : MainSideEffect
data object RefreshArchive : MainSideEffect
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/neki/android/app/main/MainScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.neki.android.core.ui.component.LoadingDialog
import com.neki.android.core.ui.compose.collectWithLifecycle
import com.neki.android.core.ui.toast.NekiToast
import com.neki.android.feature.archive.api.ArchiveNavKey
import com.neki.android.feature.archive.api.ArchiveResult
import com.neki.android.feature.map.api.MapNavKey
import com.neki.android.feature.mypage.api.MyPageNavKey
import com.neki.android.feature.photo_upload.api.PhotoUploadNavKey
Expand Down Expand Up @@ -85,6 +86,7 @@ fun MainRoute(
is MainSideEffect.NavigateToUploadAlbumWithGallery -> navigateToUploadAlbumWithGallery(sideEffect.uriStrings)
is MainSideEffect.NavigateToUploadAlbumWithQRScan -> navigateToUploadAlbumWithQRScan(sideEffect.imageUrl)
is MainSideEffect.ShowToast -> nekiToast.showToast(sideEffect.message)
MainSideEffect.RefreshArchive -> resultBus.sendResult<ArchiveResult>(result = ArchiveResult.PhotoUploaded)
}
}

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/neki/android/app/main/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class MainViewModel @Inject constructor(
.onSuccess {
reduce { copy(isLoading = false, scannedImageUrl = null) }
postSideEffect(MainSideEffect.ShowToast("이미지를 추가했어요"))
postSideEffect(MainSideEffect.RefreshArchive)
}
.onFailure { e ->
Timber.e(e)
Expand All @@ -125,6 +126,7 @@ class MainViewModel @Inject constructor(
.onSuccess {
reduce { copy(isLoading = false, selectedUris = persistentListOf()) }
postSideEffect(MainSideEffect.ShowToast("이미지를 추가했어요"))
postSideEffect(MainSideEffect.RefreshArchive)
}
.onFailure { e ->
Timber.e(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.ProvidedValue
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateMapOf
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.BUFFERED
Expand Down Expand Up @@ -34,7 +35,7 @@ object LocalResultEventBus {
*/
// https://github.com/android/nav3-recipes/blob/main/app/src/main/java/com/example/nav3recipes/results/event/README.md
class ResultEventBus {
val channelMap: MutableMap<String, Channel<Any?>> = mutableMapOf()
val channelMap = mutableStateMapOf<String, Channel<Any?>>()

inline fun <reified T> getResultFlow(resultKey: String = T::class.toString()) =
channelMap[resultKey]?.receiveAsFlow()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ sealed interface ArchiveResult {
}

data class FavoriteChanged(val photoId: Long, val isFavorite: Boolean) : ArchiveResult

data object PhotoUploaded : ArchiveResult
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package com.neki.android.feature.archive.impl.main

import android.net.Uri
import androidx.compose.foundation.text.input.TextFieldState
import com.neki.android.core.model.AlbumPreview
import com.neki.android.core.model.Photo
import com.neki.android.core.model.UploadType
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

Expand All @@ -14,30 +12,19 @@ data class ArchiveMainState(
val favoriteAlbum: AlbumPreview = AlbumPreview(title = "즐겨찾는사진"),
val albums: ImmutableList<AlbumPreview> = persistentListOf(),
val recentPhotos: ImmutableList<Photo> = persistentListOf(),
val scannedImageUrl: String? = null,
val selectedUris: ImmutableList<Uri> = persistentListOf(),
val isShowSelectWithAlbumDialog: Boolean = false,
val isShowAddAlbumBottomSheet: Boolean = false,
val albumNameTextState: TextFieldState = TextFieldState(),
) {
val uploadType: UploadType
get() = if (scannedImageUrl != null) UploadType.QR_CODE else UploadType.GALLERY
}
)

sealed interface ArchiveMainIntent {
data object EnterArchiveMainScreen : ArchiveMainIntent
data object RefreshArchiveMainScreen : ArchiveMainIntent
data object RefreshArchiveMainPhotos : ArchiveMainIntent
data object ClickScreen : ArchiveMainIntent
data object ClickGoToTopButton : ArchiveMainIntent

// TopBar Intent
data object DismissToolTipPopup : ArchiveMainIntent
data object ClickQRScanIcon : ArchiveMainIntent

data class SelectGalleryImage(val uris: List<Uri>) : ArchiveMainIntent
data object DismissSelectWithAlbumDialog : ArchiveMainIntent
data object ClickUploadWithAlbumRow : ArchiveMainIntent
data object ClickUploadWithoutAlbumRow : ArchiveMainIntent
data object ClickNotificationIcon : ArchiveMainIntent

// Album Intent
Expand All @@ -54,23 +41,16 @@ sealed interface ArchiveMainIntent {
// Add Album BottomSheet Intent
data object DismissAddAlbumBottomSheet : ArchiveMainIntent
data object ClickAddAlbumButton : ArchiveMainIntent

// Result
data class QRCodeScanned(val imageUrl: String) : ArchiveMainIntent
data object ReceiveOpenGalleryResult : ArchiveMainIntent
}

sealed interface ArchiveMainSideEffect {
data object NavigateToQRScan : ArchiveMainSideEffect
data class NavigateToUploadAlbumWithGallery(val uriStrings: List<String>) : ArchiveMainSideEffect
data class NavigateToUploadAlbumWithQRScan(val imageUrl: String) : ArchiveMainSideEffect
data object NavigateToAllAlbum : ArchiveMainSideEffect
data class NavigateToFavoriteAlbum(val albumId: Long) : ArchiveMainSideEffect
data class NavigateToAlbumDetail(val albumId: Long, val title: String) : ArchiveMainSideEffect
data object NavigateToAllPhoto : ArchiveMainSideEffect
data class NavigateToPhotoDetail(val photos: List<Photo>, val index: Int) : ArchiveMainSideEffect

data object ScrollToTop : ArchiveMainSideEffect
data object OpenGallery : ArchiveMainSideEffect
data class ShowToastMessage(val message: String) : ArchiveMainSideEffect
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package com.neki.android.feature.archive.impl.main

import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -40,23 +37,18 @@ import com.neki.android.core.ui.toast.NekiToast
import com.neki.android.feature.archive.impl.component.AddAlbumBottomSheet
import com.neki.android.feature.archive.impl.const.ArchiveConst.ARCHIVE_GRID_ITEM_SPACING
import com.neki.android.feature.archive.impl.const.ArchiveConst.PHOTO_GRAY_LAYOUT_BOTTOM_PADDING
import com.neki.android.feature.archive.impl.main.component.AlbumUploadOption
import com.neki.android.feature.archive.impl.main.component.ArchiveMainAlbumList
import com.neki.android.feature.archive.impl.main.component.ArchiveMainPhotoItem
import com.neki.android.feature.archive.impl.main.component.ArchiveMainTitleRow
import com.neki.android.feature.archive.impl.main.component.ArchiveMainTopBar
import com.neki.android.feature.archive.impl.component.EmptyPhotoContent
import com.neki.android.feature.archive.impl.main.component.GotoTopButton
import com.neki.android.feature.archive.impl.main.component.SelectWithAlbumDialog
import kotlinx.collections.immutable.persistentListOf
import timber.log.Timber

@Composable
internal fun ArchiveMainRoute(
viewModel: ArchiveMainViewModel = hiltViewModel(),
navigateToQRScan: () -> Unit,
navigateToUploadAlbumWithGallery: (List<String>) -> Unit,
navigateToUploadAlbumWithQRScan: (String) -> Unit,
navigateToAllAlbum: () -> Unit,
navigateToFavoriteAlbum: (Long) -> Unit,
navigateToAlbumDetail: (Long, String) -> Unit,
Expand All @@ -67,19 +59,10 @@ internal fun ArchiveMainRoute(
val context = LocalContext.current
val lazyState = rememberLazyStaggeredGridState()
val nekiToast = remember { NekiToast(context) }
val photoPicker = rememberLauncherForActivityResult(ActivityResultContracts.PickMultipleVisualMedia(10)) { uris ->
if (uris.isNotEmpty()) {
viewModel.store.onIntent(ArchiveMainIntent.SelectGalleryImage(uris))
} else {
Timber.d("No media selected")
}
}

viewModel.store.sideEffects.collectWithLifecycle { sideEffect ->
when (sideEffect) {
ArchiveMainSideEffect.NavigateToQRScan -> navigateToQRScan()
is ArchiveMainSideEffect.NavigateToUploadAlbumWithGallery -> navigateToUploadAlbumWithGallery(sideEffect.uriStrings)
is ArchiveMainSideEffect.NavigateToUploadAlbumWithQRScan -> navigateToUploadAlbumWithQRScan(sideEffect.imageUrl)
ArchiveMainSideEffect.NavigateToAllAlbum -> navigateToAllAlbum()
is ArchiveMainSideEffect.NavigateToFavoriteAlbum -> navigateToFavoriteAlbum(sideEffect.albumId)
is ArchiveMainSideEffect.NavigateToAlbumDetail -> navigateToAlbumDetail(sideEffect.albumId, sideEffect.title)
Expand All @@ -88,7 +71,6 @@ internal fun ArchiveMainRoute(
ArchiveNavKey.PhotoDetail(photos = sideEffect.photos, initialIndex = sideEffect.index),
)
ArchiveMainSideEffect.ScrollToTop -> lazyState.animateScrollToItem(0)
ArchiveMainSideEffect.OpenGallery -> photoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
is ArchiveMainSideEffect.ShowToastMessage -> nekiToast.showToast(text = sideEffect.message)
}
}
Expand All @@ -115,7 +97,6 @@ internal fun ArchiveMainScreen(
ArchiveMainTopBar(
showTooltip = uiState.isFirstEntered,
onClickQRCodeIcon = { onIntent(ArchiveMainIntent.ClickQRScanIcon) },
// onClickNotificationIcon = { onIntent(ArchiveMainIntent.ClickNotificationIcon) },
onDismissToolTipPopup = { onIntent(ArchiveMainIntent.DismissToolTipPopup) },
)
ArchiveMainContent(
Expand Down Expand Up @@ -166,18 +147,6 @@ internal fun ArchiveMainScreen(
errorMessage = errorMessage,
)
}

if (uiState.isShowSelectWithAlbumDialog) {
SelectWithAlbumDialog(
onDismissRequest = { onIntent(ArchiveMainIntent.DismissSelectWithAlbumDialog) },
onSelect = { option ->
when (option) {
AlbumUploadOption.WITHOUT_ALBUM -> onIntent(ArchiveMainIntent.ClickUploadWithoutAlbumRow)
AlbumUploadOption.WITH_ALBUM -> onIntent(ArchiveMainIntent.ClickUploadWithAlbumRow)
}
},
)
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
package com.neki.android.feature.archive.impl.main

import android.net.Uri
import androidx.compose.foundation.text.input.TextFieldState
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.neki.android.core.dataapi.repository.FolderRepository
import com.neki.android.core.dataapi.repository.PhotoRepository
import com.neki.android.core.dataapi.repository.UserRepository
import com.neki.android.core.domain.usecase.UploadMultiplePhotoUseCase
import com.neki.android.core.domain.usecase.UploadSinglePhotoUseCase
import com.neki.android.core.model.Photo
import com.neki.android.core.model.UploadType
import com.neki.android.core.ui.MviIntentStore
import com.neki.android.core.ui.mviIntentStore
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
Expand All @@ -28,8 +23,6 @@ private const val DEFAULT_PHOTOS_SIZE = 20

@HiltViewModel
class ArchiveMainViewModel @Inject constructor(
private val uploadSinglePhotoUseCase: UploadSinglePhotoUseCase,
private val uploadMultiplePhotoUseCase: UploadMultiplePhotoUseCase,
private val photoRepository: PhotoRepository,
private val folderRepository: FolderRepository,
private val userRepository: UserRepository,
Expand All @@ -52,7 +45,7 @@ class ArchiveMainViewModel @Inject constructor(
if (intent != ArchiveMainIntent.EnterArchiveMainScreen) reduce { copy(isFirstEntered = false) }
when (intent) {
ArchiveMainIntent.EnterArchiveMainScreen -> fetchInitialData(reduce)
ArchiveMainIntent.RefreshArchiveMainScreen -> fetchInitialData(reduce)
ArchiveMainIntent.RefreshArchiveMainPhotos -> viewModelScope.launch { fetchPhotos(reduce) }
ArchiveMainIntent.ClickScreen -> reduce { copy(isFirstEntered = false) }
ArchiveMainIntent.ClickGoToTopButton -> postSideEffect(ArchiveMainSideEffect.ScrollToTop)

Expand All @@ -63,30 +56,6 @@ class ArchiveMainViewModel @Inject constructor(
}

ArchiveMainIntent.ClickQRScanIcon -> postSideEffect(ArchiveMainSideEffect.NavigateToQRScan)

is ArchiveMainIntent.SelectGalleryImage -> reduce {
copy(
isShowSelectWithAlbumDialog = true,
selectedUris = intent.uris.toImmutableList(),
)
}

ArchiveMainIntent.DismissSelectWithAlbumDialog -> reduce { copy(isShowSelectWithAlbumDialog = false) }
ArchiveMainIntent.ClickUploadWithAlbumRow -> {
reduce {
copy(
isShowSelectWithAlbumDialog = false,
scannedImageUrl = null,
selectedUris = persistentListOf(),
)
}
if (state.scannedImageUrl == null)
postSideEffect(ArchiveMainSideEffect.NavigateToUploadAlbumWithGallery(state.selectedUris.map { it.toString() }))
else postSideEffect(ArchiveMainSideEffect.NavigateToUploadAlbumWithQRScan(state.scannedImageUrl))
}

ArchiveMainIntent.ClickUploadWithoutAlbumRow -> uploadWithoutAlbum(state, reduce, postSideEffect)

ArchiveMainIntent.ClickNotificationIcon -> {}

// Album Intent
Expand All @@ -103,16 +72,6 @@ class ArchiveMainViewModel @Inject constructor(
// Add Album BottomSheet Intent
ArchiveMainIntent.DismissAddAlbumBottomSheet -> reduce { copy(isShowAddAlbumBottomSheet = false) }
ArchiveMainIntent.ClickAddAlbumButton -> handleAddAlbum(state.albumNameTextState.text.trim().toString(), reduce, postSideEffect)

// Result
is ArchiveMainIntent.QRCodeScanned -> reduce {
copy(
scannedImageUrl = intent.imageUrl,
isShowSelectWithAlbumDialog = true,
)
}

ArchiveMainIntent.ReceiveOpenGalleryResult -> postSideEffect(ArchiveMainSideEffect.OpenGallery)
}
}

Expand Down Expand Up @@ -151,7 +110,7 @@ class ArchiveMainViewModel @Inject constructor(
}

private suspend fun fetchPhotos(reduce: (ArchiveMainState.() -> ArchiveMainState) -> Unit, size: Int = DEFAULT_PHOTOS_SIZE) {
photoRepository.getPhotos()
photoRepository.getPhotos(size = size)
.onSuccess { data ->
reduce { copy(recentPhotos = data.toImmutableList()) }
}
Expand All @@ -170,77 +129,6 @@ class ArchiveMainViewModel @Inject constructor(
}
}

private fun uploadWithoutAlbum(
state: ArchiveMainState,
reduce: (ArchiveMainState.() -> ArchiveMainState) -> Unit,
postSideEffect: (ArchiveMainSideEffect) -> Unit,
) {
reduce { copy(isShowSelectWithAlbumDialog = false) }
val onSuccessSideEffect = {
reduce { copy(isLoading = false) }
postSideEffect(ArchiveMainSideEffect.ShowToastMessage("이미지를 추가했어요"))
}
if (state.uploadType == UploadType.QR_CODE) {
uploadSingleImage(
imageUrl = state.scannedImageUrl ?: return,
reduce = reduce,
postSideEffect = postSideEffect,
onSuccess = onSuccessSideEffect,
)
} else {
uploadMultipleImages(
imageUris = state.selectedUris,
reduce = reduce,
postSideEffect = postSideEffect,
onSuccess = onSuccessSideEffect,
)
}
}

private fun uploadSingleImage(
imageUrl: String,
reduce: (ArchiveMainState.() -> ArchiveMainState) -> Unit,
postSideEffect: (ArchiveMainSideEffect) -> Unit,
onSuccess: () -> Unit,
) {
viewModelScope.launch {
reduce { copy(isLoading = true) }

uploadSinglePhotoUseCase(
imageUrl = imageUrl,
).onSuccess {
fetchPhotos(reduce, 1) // 가장 최신 데이터 가져오기
onSuccess()
}.onFailure { e ->
Timber.e(e)
postSideEffect(ArchiveMainSideEffect.ShowToastMessage("이미지 업로드에 실패했어요"))
reduce { copy(isLoading = false) }
}
}
}

private fun uploadMultipleImages(
imageUris: List<Uri>,
reduce: (ArchiveMainState.() -> ArchiveMainState) -> Unit,
postSideEffect: (ArchiveMainSideEffect) -> Unit,
onSuccess: () -> Unit,
) {
viewModelScope.launch {
reduce { copy(isLoading = true) }

uploadMultiplePhotoUseCase(
imageUris = imageUris,
).onSuccess {
fetchPhotos(reduce)
onSuccess()
}.onFailure { e ->
Timber.e(e)
postSideEffect(ArchiveMainSideEffect.ShowToastMessage("이미지 업로드에 실패했어요"))
reduce { copy(isLoading = false) }
}
}
}

private fun handleFavoriteToggle(
photo: Photo,
reduce: (ArchiveMainState.() -> ArchiveMainState) -> Unit,
Expand Down
Loading