diff --git a/core/model/src/main/java/com/susu/core/model/EnvelopeSearch.kt b/core/model/src/main/java/com/susu/core/model/EnvelopeSearch.kt index 22392430..6b6538af 100644 --- a/core/model/src/main/java/com/susu/core/model/EnvelopeSearch.kt +++ b/core/model/src/main/java/com/susu/core/model/EnvelopeSearch.kt @@ -1,5 +1,8 @@ package com.susu.core.model +import androidx.compose.runtime.Stable + +@Stable data class EnvelopeSearch( val envelope: Envelope, val category: Category? = null, diff --git a/core/model/src/main/java/com/susu/core/model/FriendStatistics.kt b/core/model/src/main/java/com/susu/core/model/FriendStatistics.kt index 5b189b31..84cd8157 100644 --- a/core/model/src/main/java/com/susu/core/model/FriendStatistics.kt +++ b/core/model/src/main/java/com/susu/core/model/FriendStatistics.kt @@ -1,5 +1,8 @@ package com.susu.core.model +import androidx.compose.runtime.Stable + +@Stable data class FriendStatistics( val friend: Friend = Friend(), val receivedAmounts: Int = 0, diff --git a/data/src/main/java/com/susu/data/data/repository/EnvelopesRepositoryImpl.kt b/data/src/main/java/com/susu/data/data/repository/EnvelopesRepositoryImpl.kt index 03f08459..4ecbab24 100644 --- a/data/src/main/java/com/susu/data/data/repository/EnvelopesRepositoryImpl.kt +++ b/data/src/main/java/com/susu/data/data/repository/EnvelopesRepositoryImpl.kt @@ -20,8 +20,8 @@ class EnvelopesRepositoryImpl @Inject constructor( ) : EnvelopesRepository { override suspend fun getEnvelopesList( friendIds: List?, - fromTotalAmounts: Int?, - toTotalAmounts: Int?, + fromTotalAmounts: Long?, + toTotalAmounts: Long?, page: Int?, size: Int?, sort: String?, diff --git a/data/src/main/java/com/susu/data/remote/api/EnvelopesService.kt b/data/src/main/java/com/susu/data/remote/api/EnvelopesService.kt index 0e8aca80..f70ae8fa 100644 --- a/data/src/main/java/com/susu/data/remote/api/EnvelopesService.kt +++ b/data/src/main/java/com/susu/data/remote/api/EnvelopesService.kt @@ -21,8 +21,8 @@ interface EnvelopesService { @GET("envelopes/friend-statistics") suspend fun getEnvelopesList( @Query("friendIds") friendIds: List?, - @Query("fromTotalAmounts") fromTotalAmounts: Int?, - @Query("toTotalAmounts") toTotalAmounts: Int?, + @Query("fromTotalAmounts") fromTotalAmounts: Long?, + @Query("toTotalAmounts") toTotalAmounts: Long?, @Query("page") page: Int?, @Query("size") size: Int?, @Query("sort") sort: String?, diff --git a/domain/src/main/java/com/susu/domain/repository/EnvelopesRepository.kt b/domain/src/main/java/com/susu/domain/repository/EnvelopesRepository.kt index e002d581..6a3c9991 100644 --- a/domain/src/main/java/com/susu/domain/repository/EnvelopesRepository.kt +++ b/domain/src/main/java/com/susu/domain/repository/EnvelopesRepository.kt @@ -12,8 +12,8 @@ import kotlinx.datetime.LocalDateTime interface EnvelopesRepository { suspend fun getEnvelopesList( friendIds: List?, - fromTotalAmounts: Int?, - toTotalAmounts: Int?, + fromTotalAmounts: Long?, + toTotalAmounts: Long?, page: Int?, size: Int?, sort: String?, diff --git a/domain/src/main/java/com/susu/domain/usecase/envelope/GetEnvelopesListUseCase.kt b/domain/src/main/java/com/susu/domain/usecase/envelope/GetEnvelopesListUseCase.kt index f9ce7911..480d76ce 100644 --- a/domain/src/main/java/com/susu/domain/usecase/envelope/GetEnvelopesListUseCase.kt +++ b/domain/src/main/java/com/susu/domain/usecase/envelope/GetEnvelopesListUseCase.kt @@ -22,8 +22,8 @@ class GetEnvelopesListUseCase @Inject constructor( data class Param( val friendIds: List? = emptyList(), - val fromTotalAmounts: Int? = null, - val toTotalAmounts: Int? = null, + val fromTotalAmounts: Long? = null, + val toTotalAmounts: Long? = null, val page: Int? = null, val size: Int? = null, val sort: String? = null, diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt index dbd29a7b..5fcbd29e 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt @@ -107,6 +107,7 @@ internal fun MainScreen( navigator.popBackStackIfNotHome() }, navigateSentEnvelopeSearch = navigator::navigateSentEnvelopeSearch, + navigateEnvelopeFilter = navigator::navigateEnvelopeFilter, handleException = viewModel::handleException, onShowSnackbar = viewModel::onShowSnackbar, onShowDialog = viewModel::onShowDialog, diff --git a/feature/sent/src/main/java/com/susu/feature/envelopefilter/EnvelopeFilterContract.kt b/feature/sent/src/main/java/com/susu/feature/envelopefilter/EnvelopeFilterContract.kt index 2bcc7abf..1b396575 100644 --- a/feature/sent/src/main/java/com/susu/feature/envelopefilter/EnvelopeFilterContract.kt +++ b/feature/sent/src/main/java/com/susu/feature/envelopefilter/EnvelopeFilterContract.kt @@ -14,6 +14,7 @@ data class EnvelopeFilterState( val toAmount: Long? = null, val maxFromAmount: Long = 0, val maxToAmount: Long = 0, + val isSent: Boolean = false, ) : UiState { val sliderValue = fromAmount?.toFloat()?.rangeTo(toAmount?.toFloat()!!) ?: maxFromAmount.toFloat()..maxToAmount.toFloat() val sliderValueRange = maxFromAmount.toFloat()..maxToAmount.toFloat() diff --git a/feature/sent/src/main/java/com/susu/feature/envelopefilter/EnvelopeFilterScreen.kt b/feature/sent/src/main/java/com/susu/feature/envelopefilter/EnvelopeFilterScreen.kt index 8272381f..d4999191 100644 --- a/feature/sent/src/main/java/com/susu/feature/envelopefilter/EnvelopeFilterScreen.kt +++ b/feature/sent/src/main/java/com/susu/feature/envelopefilter/EnvelopeFilterScreen.kt @@ -139,7 +139,13 @@ fun EnvelopeFilterScreen( if (uiState.maxFromAmount != uiState.maxToAmount) { Text( - text = stringResource(R.string.envelope_filter_screen_money), + text = stringResource( + if (uiState.isSent) { + R.string.envelope_filter_total_money + } else { + R.string.envelope_filter_screen_money + }, + ), style = SusuTheme.typography.title_xs, ) Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_m)) diff --git a/feature/sent/src/main/java/com/susu/feature/envelopefilter/EnvelopeFilterViewModel.kt b/feature/sent/src/main/java/com/susu/feature/envelopefilter/EnvelopeFilterViewModel.kt index 78999fcd..d6585ca8 100644 --- a/feature/sent/src/main/java/com/susu/feature/envelopefilter/EnvelopeFilterViewModel.kt +++ b/feature/sent/src/main/java/com/susu/feature/envelopefilter/EnvelopeFilterViewModel.kt @@ -53,6 +53,7 @@ class EnvelopeFilterViewModel @Inject constructor( maxToAmount = maxToAmount, fromAmount = filter.fromAmount, toAmount = filter.toAmount, + isSent = filter.isSent, ) } } diff --git a/feature/sent/src/main/java/com/susu/feature/sent/SentContract.kt b/feature/sent/src/main/java/com/susu/feature/sent/SentContract.kt index b3147fe4..c20d29a3 100644 --- a/feature/sent/src/main/java/com/susu/feature/sent/SentContract.kt +++ b/feature/sent/src/main/java/com/susu/feature/sent/SentContract.kt @@ -1,8 +1,10 @@ package com.susu.feature.sent +import androidx.annotation.StringRes import com.susu.core.model.EnvelopeSearch import com.susu.core.model.Friend import com.susu.core.model.FriendStatistics +import com.susu.core.ui.R import com.susu.core.ui.base.SideEffect import com.susu.core.ui.base.UiState import kotlinx.collections.immutable.PersistentList @@ -12,7 +14,14 @@ data class SentState( val isLoading: Boolean = false, val envelopesList: PersistentList = persistentListOf(), val showEmptyEnvelopes: Boolean = false, -) : UiState + val selectedFriendList: PersistentList = persistentListOf(), + val fromAmount: Long? = null, + val toAmount: Long? = null, + val selectedAlignPosition: Int = EnvelopeAlign.RECENT.ordinal, + val showAlignBottomSheet: Boolean = false, +) : UiState { + val isFiltered = fromAmount != null || toAmount != null || selectedFriendList.isNotEmpty() +} data class FriendStatisticsState( val friend: Friend = Friend(), @@ -30,8 +39,32 @@ internal fun FriendStatistics.toState() = FriendStatisticsState( totalAmounts = totalAmounts, ) +enum class EnvelopeAlign( + @StringRes val stringResId: Int, + val query: String, +) { + RECENT( + stringResId = R.string.word_align_recently, + query = "createdAt,desc", + ), + OUTDATED( + stringResId = R.string.word_align_outdated, + query = "createdAt,asc", + ), + HIGH_AMOUNT( + stringResId = R.string.word_align_high_amount, + query = "totalSentAmounts,desc", + ), + LOW_AMOUNT( + stringResId = R.string.word_align_low_amount, + query = "totalSentAmounts,asc", + ), +} + sealed interface SentEffect : SideEffect { data class NavigateEnvelope(val id: Long) : SentEffect data object NavigateEnvelopeAdd : SentEffect data object NavigateEnvelopeSearch : SentEffect + data class NavigateEnvelopeFilter(val filter: String) : SentEffect + data object ScrollToTop : SentEffect } diff --git a/feature/sent/src/main/java/com/susu/feature/sent/SentScreen.kt b/feature/sent/src/main/java/com/susu/feature/sent/SentScreen.kt index a5d4cdfc..f701b554 100644 --- a/feature/sent/src/main/java/com/susu/feature/sent/SentScreen.kt +++ b/feature/sent/src/main/java/com/susu/feature/sent/SentScreen.kt @@ -1,6 +1,7 @@ package com.susu.feature.sent import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -9,18 +10,24 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -29,51 +36,68 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.susu.core.designsystem.component.appbar.SusuDefaultAppBar import com.susu.core.designsystem.component.appbar.icon.LogoIcon -import com.susu.core.designsystem.component.appbar.icon.NotificationIcon import com.susu.core.designsystem.component.appbar.icon.SearchIcon +import com.susu.core.designsystem.component.bottomsheet.SusuSelectionBottomSheet +import com.susu.core.designsystem.component.button.FilledButtonColor +import com.susu.core.designsystem.component.button.FilterButton import com.susu.core.designsystem.component.button.GhostButtonColor +import com.susu.core.designsystem.component.button.SelectedFilterButton import com.susu.core.designsystem.component.button.SmallButtonStyle import com.susu.core.designsystem.component.button.SusuFloatingButton import com.susu.core.designsystem.component.button.SusuGhostButton -import com.susu.core.designsystem.theme.Gray100 import com.susu.core.designsystem.theme.Gray50 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.model.Friend import com.susu.core.ui.extension.OnBottomReached import com.susu.core.ui.extension.collectWithLifecycle +import com.susu.core.ui.extension.toMoneyFormat import com.susu.feature.sent.component.SentCard +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.android.awaitFrame +import kotlinx.coroutines.launch +import me.onebone.toolbar.CollapsingToolbarScaffold +import me.onebone.toolbar.ScrollStrategy +import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState @Composable fun SentRoute( viewModel: SentViewModel = hiltViewModel(), deletedFriendId: Long?, refresh: Boolean?, + filter: String?, padding: PaddingValues, navigateSentEnvelope: (Long) -> Unit, navigateSentEnvelopeAdd: () -> Unit, navigateSentEnvelopeSearch: () -> Unit, + navigateEnvelopeFilter: (String) -> Unit, ) { val uiState = viewModel.uiState.collectAsStateWithLifecycle().value val envelopesListState = rememberLazyListState() + val scope = rememberCoroutineScope() viewModel.sideEffect.collectWithLifecycle { sideEffect -> when (sideEffect) { SentEffect.NavigateEnvelopeAdd -> navigateSentEnvelopeAdd() is SentEffect.NavigateEnvelope -> navigateSentEnvelope(sideEffect.id) SentEffect.NavigateEnvelopeSearch -> navigateSentEnvelopeSearch() + is SentEffect.NavigateEnvelopeFilter -> navigateEnvelopeFilter(sideEffect.filter) + SentEffect.ScrollToTop -> scope.launch { + awaitFrame() + envelopesListState.animateScrollToItem(0) + } } } LaunchedEffect(key1 = Unit) { - viewModel.getEnvelopesList(refresh) if (deletedFriendId != null) { viewModel.deleteEmptyFriendStatistics(deletedFriendId) } + viewModel.getEnvelopesList(refresh) + viewModel.filterIfNeed(filter) } envelopesListState.OnBottomReached { - if (uiState.envelopesList.isNotEmpty()) { - viewModel.getEnvelopesList(refresh = false) - } + viewModel.getEnvelopesList(refresh = false) } SentScreen( @@ -86,126 +110,178 @@ fun SentRoute( onClickHistory = { friendId -> viewModel.getEnvelopesHistoryList(friendId) }, + onClickFilterButton = viewModel::navigateEnvelopeFilter, + onClickFriendClose = viewModel::unselectFriend, + onClickMoneyClose = viewModel::removeMoney, + onClickAlignButton = viewModel::showAlignBottomSheet, + onClickAlignBottomSheetItem = viewModel::updateAlignPosition, + onDismissAlignBottomSheet = viewModel::hideAlignBottomSheet, ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SentScreen( - modifier: Modifier = Modifier, uiState: SentState = SentState(), envelopesListState: LazyListState = rememberLazyListState(), padding: PaddingValues, onClickSearchIcon: () -> Unit = {}, - onClickNotificationIcon: () -> Unit = {}, onClickHistory: (Long) -> Unit = {}, onClickHistoryShowAll: (Long) -> Unit = {}, onClickAddEnvelope: () -> Unit = {}, + onClickFilterButton: () -> Unit = {}, + onClickFriendClose: (Friend) -> Unit = {}, + onClickMoneyClose: () -> Unit = {}, + onClickAlignButton: () -> Unit = {}, + onClickAlignBottomSheetItem: (Int) -> Unit = {}, + onDismissAlignBottomSheet: () -> Unit = {}, ) { - Box( - modifier = Modifier - .background(SusuTheme.colorScheme.background15) - .padding(padding) - .fillMaxSize(), - ) { - Column { - SusuDefaultAppBar( - leftIcon = { - Spacer(modifier = modifier.size(SusuTheme.spacing.spacing_xs)) - LogoIcon() - }, - title = stringResource(R.string.sent_screen_appbar_title), - actions = { - SearchIcon(onClickSearchIcon) - NotificationIcon(onClickNotificationIcon) - }, - ) + Surface { + Box( + modifier = Modifier + .background(SusuTheme.colorScheme.background15) + .padding(padding) + .fillMaxSize(), + ) { + Column { + SusuDefaultAppBar( + leftIcon = { + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_xs)) + LogoIcon() + }, + title = stringResource(R.string.sent_screen_appbar_title), + actions = { + SearchIcon(onClickSearchIcon) + }, + ) - LazyColumn( - modifier = modifier.fillMaxSize(), - state = envelopesListState, - verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), - contentPadding = PaddingValues(SusuTheme.spacing.spacing_m), - ) { - item { - FilterSection( - padding = PaddingValues( - bottom = SusuTheme.spacing.spacing_xxs, - ), - ) - } + val state = rememberCollapsingToolbarScaffoldState() + CollapsingToolbarScaffold( + modifier = Modifier.fillMaxSize(), + state = state, + scrollStrategy = ScrollStrategy.EnterAlways, + toolbar = { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(32.dp), + ) - items( - items = uiState.envelopesList, - key = { it.friend.id }, + FilterSection( + modifier = Modifier.graphicsLayer { + alpha = state.toolbarState.progress + }, + uiState = uiState, + padding = PaddingValues( + top = SusuTheme.spacing.spacing_m, + ), + onClickAlignButton = onClickAlignButton, + onClickFilterButton = onClickFilterButton, + onClickFriendClose = onClickFriendClose, + onClickMoneyClose = onClickMoneyClose, + ) + }, ) { - SentCard( - uiState = it, - friend = it.friend, - totalAmounts = it.totalAmounts, - sentAmounts = it.sentAmounts, - receivedAmounts = it.receivedAmounts, - onClickHistory = onClickHistory, - onClickHistoryShowAll = onClickHistoryShowAll, - ) + if (uiState.showEmptyEnvelopes) { + EmptyView( + onClickAddEnvelope = onClickAddEnvelope, + ) + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = envelopesListState, + verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + contentPadding = PaddingValues(SusuTheme.spacing.spacing_m), + ) { + items( + items = uiState.envelopesList, + key = { it.friend.id }, + ) { + SentCard( + state = it, + onClickHistory = onClickHistory, + onClickHistoryShowAll = onClickHistoryShowAll, + ) + } + } + } } } - if (uiState.showEmptyEnvelopes) { - FilterSection( - padding = PaddingValues(SusuTheme.spacing.spacing_m), - ) - EmptyView( - onClickAddEnvelope = onClickAddEnvelope, + if (uiState.showAlignBottomSheet) { + SusuSelectionBottomSheet( + onDismissRequest = onDismissAlignBottomSheet, + containerHeight = 250.dp, + items = EnvelopeAlign.entries.map { stringResource(id = it.stringResId) }.toPersistentList(), + selectedItemPosition = uiState.selectedAlignPosition, + onClickItem = onClickAlignBottomSheetItem, ) } - } - SusuFloatingButton( - modifier = modifier - .align(Alignment.BottomEnd) - .padding(SusuTheme.spacing.spacing_l), - onClick = onClickAddEnvelope, - ) + SusuFloatingButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(SusuTheme.spacing.spacing_l), + onClick = onClickAddEnvelope, + ) + } } } @Composable fun FilterSection( - modifier: Modifier = Modifier, + modifier: Modifier, + uiState: SentState = SentState(), padding: PaddingValues, + onClickAlignButton: () -> Unit, + onClickFilterButton: () -> Unit, + onClickFriendClose: (Friend) -> Unit, + onClickMoneyClose: () -> Unit, ) { Row( modifier = modifier - .fillMaxWidth() - .padding(padding), + .padding( + padding, + ) + .horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), ) { + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_xxs)) + SusuGhostButton( color = GhostButtonColor.Black, style = SmallButtonStyle.height32, - text = stringResource(com.susu.core.ui.R.string.word_align_recently), - leftIcon = { - Icon( - painter = painterResource(id = R.drawable.ic_sort), - contentDescription = stringResource(com.susu.core.ui.R.string.word_align_recently), - tint = Gray100, - modifier = modifier.size(16.dp), - ) - }, - ) - SusuGhostButton( - color = GhostButtonColor.Black, - style = SmallButtonStyle.height32, - text = stringResource(com.susu.core.ui.R.string.word_filter), + text = stringResource(id = EnvelopeAlign.entries[uiState.selectedAlignPosition].stringResId), leftIcon = { Icon( - painter = painterResource(id = R.drawable.ic_filter), - contentDescription = stringResource(com.susu.core.ui.R.string.word_filter), - tint = Gray100, - modifier = modifier.size(16.dp), + painter = painterResource(id = com.susu.core.ui.R.drawable.ic_align), + contentDescription = null, ) }, + onClick = onClickAlignButton, ) + + FilterButton(uiState.isFiltered, onClickFilterButton) + + uiState.selectedFriendList.forEach { friend -> + SelectedFilterButton( + color = FilledButtonColor.Black, + style = SmallButtonStyle.height32, + name = friend.name, + onClickCloseIcon = { onClickFriendClose(friend) }, + ) + } + + if (uiState.fromAmount != null || uiState.toAmount != null) { + SelectedFilterButton( + color = FilledButtonColor.Black, + style = SmallButtonStyle.height32, + name = "${uiState.fromAmount?.toMoneyFormat() ?: ""}~${uiState.toAmount?.toMoneyFormat() ?: ""}", + onClickCloseIcon = onClickMoneyClose, + ) + } + + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_xxs)) } } diff --git a/feature/sent/src/main/java/com/susu/feature/sent/SentViewModel.kt b/feature/sent/src/main/java/com/susu/feature/sent/SentViewModel.kt index 3e19fb27..1a8fa386 100644 --- a/feature/sent/src/main/java/com/susu/feature/sent/SentViewModel.kt +++ b/feature/sent/src/main/java/com/susu/feature/sent/SentViewModel.kt @@ -1,13 +1,19 @@ package com.susu.feature.sent import androidx.lifecycle.viewModelScope +import com.susu.core.model.Friend +import com.susu.core.ui.argument.EnvelopeFilterArgument import com.susu.core.ui.base.BaseViewModel +import com.susu.core.ui.extension.decodeFromUri +import com.susu.core.ui.extension.encodeToUri import com.susu.domain.usecase.envelope.GetEnvelopesHistoryListUseCase import com.susu.domain.usecase.envelope.GetEnvelopesListUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.json.Json import javax.inject.Inject @HiltViewModel @@ -17,32 +23,68 @@ class SentViewModel @Inject constructor( ) : BaseViewModel( SentState(), ) { + private val mutex = Mutex() private var page = 0 + private var filter: EnvelopeFilterArgument = EnvelopeFilterArgument() + private var filterUri: String? = null fun getEnvelopesList(refresh: Boolean?) = viewModelScope.launch { - if (currentState.isLoading) return@launch + mutex.withLock { + val currentList = if (refresh == true) { + page = 0 + emptyList() + } else { + currentState.envelopesList + } - intent { copy(isLoading = true) } + getEnvelopesListUseCase( + GetEnvelopesListUseCase.Param( + page = page, + friendIds = currentState.selectedFriendList.map { it.id }, + fromTotalAmounts = currentState.fromAmount, + toTotalAmounts = currentState.toAmount, + sort = EnvelopeAlign.entries[currentState.selectedAlignPosition].query, + ), + ).onSuccess { envelopesList -> + page++ + val newEnvelopesList = currentList.plus(envelopesList.map { it.toState() }).toPersistentList() + intent { + copy( + envelopesList = newEnvelopesList, + showEmptyEnvelopes = newEnvelopesList.isEmpty(), + ) + } + }.onFailure { + intent { + copy( + showEmptyEnvelopes = true, + ) + } + } - if (refresh == true) { - intent { copy(envelopesList = persistentListOf()) } - page = 0 + if (refresh == true) postSideEffect(SentEffect.ScrollToTop) } + } - getEnvelopesListUseCase( - GetEnvelopesListUseCase.Param(page = page), - ).onSuccess { envelopesList -> - page++ - val newEnvelopesList = currentState.envelopesList.plus(envelopesList.map { it.toState() }).toPersistentList() - intent { - copy( - envelopesList = newEnvelopesList, - showEmptyEnvelopes = newEnvelopesList.isEmpty(), - ) - } + fun filterIfNeed(filterUri: String?) { + if (filterUri == null) return + + if (this.filterUri == filterUri) return + this.filterUri = filterUri + + val ledgerFilterArgument = Json.decodeFromUri(filterUri) + if (filter == ledgerFilterArgument) return + + filter = ledgerFilterArgument + intent { + copy( + selectedFriendList = filter.selectedFriendList.toPersistentList(), + fromAmount = filter.fromAmount, + toAmount = filter.toAmount, + ) } - intent { copy(isLoading = false) } + getEnvelopesList(true) } fun getEnvelopesHistoryList(id: Long) = viewModelScope.launch { @@ -82,4 +124,53 @@ class SentViewModel @Inject constructor( fun navigateSentEnvelope(id: Long) = postSideEffect(SentEffect.NavigateEnvelope(id = id)) fun navigateSentAdd() = postSideEffect(SentEffect.NavigateEnvelopeAdd) fun navigateSentEnvelopeSearch() = postSideEffect(SentEffect.NavigateEnvelopeSearch) + fun navigateEnvelopeFilter() = postSideEffect( + SentEffect.NavigateEnvelopeFilter( + Json.encodeToUri( + EnvelopeFilterArgument( + isSent = true, + selectedFriendList = currentState.selectedFriendList, + fromAmount = currentState.fromAmount, + toAmount = currentState.toAmount, + ), + ), + ), + ) + + fun unselectFriend(friend: Friend) { + intent { + copy( + selectedFriendList = selectedFriendList.remove(friend), + ) + } + + getEnvelopesList(true) + } + + fun removeMoney() { + intent { + copy( + fromAmount = null, + toAmount = null, + ) + } + + getEnvelopesList(true) + } + + fun showAlignBottomSheet() = intent { + copy(showAlignBottomSheet = true) + } + + fun hideAlignBottomSheet() = intent { + copy(showAlignBottomSheet = false) + } + + fun updateAlignPosition(position: Int) { + hideAlignBottomSheet() + intent { + copy(selectedAlignPosition = position) + } + getEnvelopesList(true) + } } diff --git a/feature/sent/src/main/java/com/susu/feature/sent/component/SentCard.kt b/feature/sent/src/main/java/com/susu/feature/sent/component/SentCard.kt index 3d0ca963..a3af154f 100644 --- a/feature/sent/src/main/java/com/susu/feature/sent/component/SentCard.kt +++ b/feature/sent/src/main/java/com/susu/feature/sent/component/SentCard.kt @@ -40,7 +40,6 @@ import com.susu.core.designsystem.theme.Gray60 import com.susu.core.designsystem.theme.Gray90 import com.susu.core.designsystem.theme.Orange20 import com.susu.core.designsystem.theme.SusuTheme -import com.susu.core.model.Friend import com.susu.core.ui.extension.susuClickable import com.susu.core.ui.extension.toMoneyFormat import com.susu.feature.sent.FriendStatisticsState @@ -48,19 +47,14 @@ import com.susu.feature.sent.R @Composable fun SentCard( - modifier: Modifier = Modifier, - uiState: FriendStatisticsState = FriendStatisticsState(), - friend: Friend, - totalAmounts: Int = 0, - sentAmounts: Int = 0, - receivedAmounts: Int = 0, + state: FriendStatisticsState = FriendStatisticsState(), onClickHistory: (Long) -> Unit = {}, onClickHistoryShowAll: (Long) -> Unit = {}, ) { - val degrees by animateFloatAsState(if (uiState.expand) 180f else 0f, label = "") + val degrees by animateFloatAsState(if (state.expand) 180f else 0f, label = "") Box( - modifier = modifier + modifier = Modifier .clip(shape = RoundedCornerShape(4.dp)) .fillMaxWidth() .background(SusuTheme.colorScheme.background10), @@ -69,45 +63,45 @@ fun SentCard( painter = painterResource(id = R.drawable.img_envelope), contentDescription = null, contentScale = ContentScale.Crop, - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), ) Column( - modifier = modifier + modifier = Modifier .padding(SusuTheme.spacing.spacing_m), ) { Row( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = friend.name, + text = state.friend.name, style = SusuTheme.typography.title_xs, color = Gray100, ) - Spacer(modifier = modifier.size(SusuTheme.spacing.spacing_s)) + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_s)) SusuBadge( color = BadgeColor.Gray20, - text = stringResource(R.string.sent_envelope_card_monee_total) + totalAmounts.toMoneyFormat() + + text = stringResource(R.string.sent_envelope_card_monee_total) + state.totalAmounts.toMoneyFormat() + stringResource(R.string.sent_envelope_card_money_won), padding = BadgeStyle.smallBadge, ) - Spacer(modifier = modifier.weight(1f)) + Spacer(modifier = Modifier.weight(1f)) Icon( painter = painterResource(id = R.drawable.ic_arrow_down), contentDescription = stringResource(R.string.content_description_envelope_show_history), tint = Gray100, - modifier = modifier + modifier = Modifier .clip(CircleShape) .susuClickable( onClick = { - onClickHistory(friend.id) + onClickHistory(state.friend.id) }, ) .rotate(degrees = degrees), ) } - Spacer(modifier = modifier.size(SusuTheme.spacing.spacing_m)) + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_m)) Row( - modifier = modifier + modifier = Modifier .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, @@ -124,8 +118,8 @@ fun SentCard( ) } LinearProgressIndicator( - progress = { sentAmounts.toFloat() / totalAmounts }, - modifier = modifier + progress = { state.sentAmounts.toFloat() / state.totalAmounts }, + modifier = Modifier .fillMaxWidth() .padding(vertical = SusuTheme.spacing.spacing_xxxxs), color = SusuTheme.colorScheme.primary, @@ -133,18 +127,18 @@ fun SentCard( strokeCap = StrokeCap.Round, ) Row( - modifier = modifier + modifier = Modifier .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - text = sentAmounts.toMoneyFormat() + stringResource(R.string.sent_envelope_card_money_won), + text = state.sentAmounts.toMoneyFormat() + stringResource(R.string.sent_envelope_card_money_won), style = SusuTheme.typography.title_xxxs, color = Gray90, ) Text( - text = receivedAmounts.toMoneyFormat() + stringResource(R.string.sent_envelope_card_money_won), + text = state.receivedAmounts.toMoneyFormat() + stringResource(R.string.sent_envelope_card_money_won), style = SusuTheme.typography.title_xxxs, color = Gray60, ) @@ -152,13 +146,13 @@ fun SentCard( } } AnimatedVisibility( - visible = uiState.expand, + visible = state.expand, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically(), ) { SentHistoryCard( - envelopeHistoryList = uiState.envelopesHistoryList, - friendId = friend.id, + envelopeHistoryList = state.envelopesHistoryList, + friendId = state.friend.id, onClickHistoryShowAll = onClickHistoryShowAll, ) } diff --git a/feature/sent/src/main/java/com/susu/feature/sent/navigation/SentNavigation.kt b/feature/sent/src/main/java/com/susu/feature/sent/navigation/SentNavigation.kt index c487112a..a704f825 100644 --- a/feature/sent/src/main/java/com/susu/feature/sent/navigation/SentNavigation.kt +++ b/feature/sent/src/main/java/com/susu/feature/sent/navigation/SentNavigation.kt @@ -58,6 +58,7 @@ fun NavGraphBuilder.sentNavGraph( navigateSentEnvelopeEdit: (EnvelopeDetail) -> Unit, navigateSentEnvelopeAdd: () -> Unit, navigateSentEnvelopeSearch: () -> Unit, + navigateEnvelopeFilter: (String) -> Unit, onShowSnackbar: (SnackbarToken) -> Unit, onShowDialog: (DialogToken) -> Unit, popBackStackWithFilter: (String) -> Unit, @@ -66,14 +67,19 @@ fun NavGraphBuilder.sentNavGraph( composable(route = SentRoute.route) { navBackStackEntry -> val deletedFriendId = navBackStackEntry.savedStateHandle.get(SentRoute.FRIEND_ID_ARGUMENT_NAME) val refresh = navBackStackEntry.savedStateHandle.get(SentRoute.SENT_REFRESH_ARGUMENT_NAME) + val filter = navBackStackEntry.savedStateHandle.get(SentRoute.FILTER_ENVELOPE_ARGUMENT) + navBackStackEntry.savedStateHandle.set(SentRoute.FILTER_ENVELOPE_ARGUMENT, null) + navBackStackEntry.savedStateHandle.set(SentRoute.SENT_REFRESH_ARGUMENT_NAME, null) SentRoute( padding = padding, + filter = filter, deletedFriendId = deletedFriendId, refresh = refresh, navigateSentEnvelope = navigateSentEnvelope, navigateSentEnvelopeAdd = navigateSentEnvelopeAdd, navigateSentEnvelopeSearch = navigateSentEnvelopeSearch, + navigateEnvelopeFilter = navigateEnvelopeFilter, ) } diff --git a/feature/sent/src/main/res/values/strings.xml b/feature/sent/src/main/res/values/strings.xml index f2ecea71..3a1f6e36 100644 --- a/feature/sent/src/main/res/values/strings.xml +++ b/feature/sent/src/main/res/values/strings.xml @@ -56,4 +56,5 @@ 삭제한 봉투는 다시 복구할 수 없어요 봉투가 삭제됐어요 받은 이의 연락처 + 전체 금액