Add basic overflow for list screen (#662)

This commit is contained in:
David Perez
2024-01-18 08:31:30 -06:00
committed by Álison Fernandes
parent d0d1e669d1
commit a12bc47c20
5 changed files with 253 additions and 34 deletions

View File

@@ -11,6 +11,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -22,10 +23,15 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingHandlers
import kotlinx.collections.immutable.persistentListOf
/**
* Displays the vault item listing screen.
@@ -39,6 +45,7 @@ fun VaultItemListingScreen(
onNavigateToEditSendItem: (sendId: String) -> Unit,
viewModel: VaultItemListingViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsState()
val context = LocalContext.current
val resources = context.resources
EventsEffect(viewModel = viewModel) { event ->
@@ -73,36 +80,38 @@ fun VaultItemListingScreen(
}
}
}
VaultItemListingDialogs(
dialogState = state.dialogState,
)
VaultItemListingScaffold(
state = viewModel.stateFlow.collectAsState().value,
backClick = remember(viewModel) {
{ viewModel.trySendAction(VaultItemListingsAction.BackClick) }
},
searchIconClick = remember(viewModel) {
{ viewModel.trySendAction(VaultItemListingsAction.SearchIconClick) }
},
addVaultItemClick = remember(viewModel) {
{ viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick) }
},
vaultItemClick = remember(viewModel) {
{ viewModel.trySendAction(VaultItemListingsAction.ItemClick(it)) }
},
refreshClick = remember(viewModel) {
{ viewModel.trySendAction(VaultItemListingsAction.RefreshClick) }
state = state,
vaultItemListingHandlers = remember(viewModel) {
VaultItemListingHandlers.create(viewModel)
},
)
}
@Composable
private fun VaultItemListingDialogs(
dialogState: VaultItemListingState.DialogState?,
) {
when (dialogState) {
is VaultItemListingState.DialogState.Loading -> BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(dialogState.message),
)
null -> Unit
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod")
@Composable
private fun VaultItemListingScaffold(
state: VaultItemListingState,
backClick: () -> Unit,
searchIconClick: () -> Unit,
addVaultItemClick: () -> Unit,
vaultItemClick: (id: String) -> Unit,
refreshClick: () -> Unit,
vaultItemListingHandlers: VaultItemListingHandlers,
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
@@ -115,13 +124,24 @@ private fun VaultItemListingScaffold(
scrollBehavior = scrollBehavior,
navigationIcon = painterResource(id = R.drawable.ic_back),
navigationIconContentDescription = stringResource(id = R.string.back),
onNavigationIconClick = backClick,
onNavigationIconClick = vaultItemListingHandlers.backClick,
actions = {
BitwardenSearchActionItem(
contentDescription = stringResource(id = R.string.search_vault),
onClick = searchIconClick,
onClick = vaultItemListingHandlers.searchIconClick,
)
BitwardenOverflowActionItem(
menuItemDataList = persistentListOf(
OverflowMenuItemData(
text = stringResource(id = R.string.sync),
onClick = vaultItemListingHandlers.syncClick,
),
OverflowMenuItemData(
text = stringResource(id = R.string.lock),
onClick = vaultItemListingHandlers.lockClick,
),
),
)
BitwardenOverflowActionItem()
},
)
},
@@ -129,7 +149,7 @@ private fun VaultItemListingScaffold(
if (state.itemListingType.hasFab) {
FloatingActionButton(
containerColor = MaterialTheme.colorScheme.primaryContainer,
onClick = addVaultItemClick,
onClick = vaultItemListingHandlers.addVaultItemClick,
) {
Icon(
painter = painterResource(id = R.drawable.ic_plus),
@@ -147,7 +167,7 @@ private fun VaultItemListingScaffold(
is VaultItemListingState.ViewState.Content -> {
VaultItemListingContent(
state = state.viewState,
vaultItemClick = vaultItemClick,
vaultItemClick = vaultItemListingHandlers.itemClick,
modifier = modifier,
)
}
@@ -155,7 +175,7 @@ private fun VaultItemListingScaffold(
is VaultItemListingState.ViewState.NoItems -> {
VaultItemListingEmpty(
itemListingType = state.itemListingType,
addItemClickAction = addVaultItemClick,
addItemClickAction = vaultItemListingHandlers.addVaultItemClick,
modifier = modifier,
)
}
@@ -163,7 +183,7 @@ private fun VaultItemListingScaffold(
is VaultItemListingState.ViewState.Error -> {
BitwardenErrorContent(
message = state.viewState.message(),
onTryAgainClick = refreshClick,
onTryAgainClick = vaultItemListingHandlers.refreshClick,
modifier = modifier,
)
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
@@ -22,6 +23,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
/**
@@ -43,6 +45,7 @@ class VaultItemListingViewModel @Inject constructor(
viewState = VaultItemListingState.ViewState.Loading,
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
dialogState = null,
),
) {
@@ -61,6 +64,8 @@ class VaultItemListingViewModel @Inject constructor(
override fun handleAction(action: VaultItemListingsAction) {
when (action) {
is VaultItemListingsAction.BackClick -> handleBackClick()
is VaultItemListingsAction.LockClick -> handleLockClick()
is VaultItemListingsAction.SyncClick -> handleSyncClick()
is VaultItemListingsAction.SearchIconClick -> handleSearchIconClick()
is VaultItemListingsAction.ItemClick -> handleItemClick(action)
is VaultItemListingsAction.AddVaultItemClick -> handleAddVaultItemClick()
@@ -108,6 +113,21 @@ class VaultItemListingViewModel @Inject constructor(
)
}
private fun handleLockClick() {
vaultRepository.lockVaultForCurrentUser()
}
private fun handleSyncClick() {
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Loading(
message = R.string.syncing.asText(),
),
)
}
vaultRepository.sync()
}
private fun handleSearchIconClick() {
sendEvent(
event = VaultItemListingEvent.NavigateToVaultSearchScreen,
@@ -134,27 +154,28 @@ class VaultItemListingViewModel @Inject constructor(
}
vaultRepository.vaultDataStateFlow.value.data?.let { vaultData ->
updateStateWithVaultData(vaultData)
updateStateWithVaultData(vaultData, clearDialogState = false)
}
}
//endregion VaultItemListing Handlers
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
if (vaultData.data != null) {
updateStateWithVaultData(vaultData = vaultData.data)
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true)
} else {
mutableStateFlow.update {
it.copy(
viewState = VaultItemListingState.ViewState.Error(
message = R.string.generic_error_message.asText(),
),
dialogState = null,
)
}
}
}
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) {
updateStateWithVaultData(vaultData = vaultData.data)
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true)
}
private fun vaultLoadingReceive() {
@@ -163,7 +184,7 @@ class VaultItemListingViewModel @Inject constructor(
private fun vaultNoNetworkReceive(vaultData: DataState.NoNetwork<VaultData>) {
if (vaultData.data != null) {
updateStateWithVaultData(vaultData = vaultData.data)
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true)
} else {
mutableStateFlow.update { currentState ->
currentState.copy(
@@ -172,16 +193,17 @@ class VaultItemListingViewModel @Inject constructor(
.asText()
.concat(R.string.internet_connection_required_message.asText()),
),
dialogState = null,
)
}
}
}
private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) {
updateStateWithVaultData(vaultData = vaultData.data)
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = false)
}
private fun updateStateWithVaultData(vaultData: VaultData) {
private fun updateStateWithVaultData(vaultData: VaultData, clearDialogState: Boolean) {
mutableStateFlow.update { currentState ->
currentState.copy(
itemListingType = currentState
@@ -214,6 +236,7 @@ class VaultItemListingViewModel @Inject constructor(
.toViewState()
}
},
dialogState = currentState.dialogState.takeUnless { clearDialogState },
)
}
}
@@ -227,8 +250,23 @@ data class VaultItemListingState(
val viewState: ViewState,
val baseIconUrl: String,
val isIconLoadingDisabled: Boolean,
val dialogState: DialogState?,
) {
/**
* Represents the current state of any dialogs on the screen.
*/
sealed class DialogState : Parcelable {
/**
* Represents a loading dialog with the given [message].
*/
@Parcelize
data class Loading(
val message: Text,
) : DialogState()
}
/**
* Represents the specific view states for the [VaultItemListingScreen].
*/
@@ -450,6 +488,16 @@ sealed class VaultItemListingsAction {
*/
data object RefreshClick : VaultItemListingsAction()
/**
* Click the lock button.
*/
data object LockClick : VaultItemListingsAction()
/**
* Click the refresh button.
*/
data object SyncClick : VaultItemListingsAction()
/**
* Click the back button.
*/

View File

@@ -0,0 +1,41 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingViewModel
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingsAction
/**
* A collection of handler functions for managing actions within the context of viewing a list of
* items.
*/
data class VaultItemListingHandlers(
val backClick: () -> Unit,
val searchIconClick: () -> Unit,
val addVaultItemClick: () -> Unit,
val itemClick: (id: String) -> Unit,
val refreshClick: () -> Unit,
val syncClick: () -> Unit,
val lockClick: () -> Unit,
) {
companion object {
/**
* Creates an instance of [VaultItemListingHandlers] by binding actions to the provided
* [VaultItemListingViewModel].
*/
fun create(
viewModel: VaultItemListingViewModel,
): VaultItemListingHandlers =
VaultItemListingHandlers(
backClick = { viewModel.trySendAction(VaultItemListingsAction.BackClick) },
searchIconClick = {
viewModel.trySendAction(VaultItemListingsAction.SearchIconClick)
},
addVaultItemClick = {
viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick)
},
itemClick = { viewModel.trySendAction(VaultItemListingsAction.ItemClick(it)) },
refreshClick = { viewModel.trySendAction(VaultItemListingsAction.RefreshClick) },
syncClick = { viewModel.trySendAction(VaultItemListingsAction.SyncClick) },
lockClick = { viewModel.trySendAction(VaultItemListingsAction.LockClick) },
)
}
}