mirror of
https://github.com/bitwarden/android.git
synced 2026-04-25 07:18:09 -05:00
Implement context menu on item long press (#31)
This commit is contained in:
@@ -21,6 +21,7 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
@@ -45,6 +46,7 @@ import com.x8bit.bitwarden.authenticator.ui.platform.components.button.Bitwarden
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.fab.ExpandableFabIcon
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.fab.ExpandableFloatingActionButton
|
||||
@@ -95,6 +97,24 @@ fun ItemListingScreen(
|
||||
}
|
||||
}
|
||||
|
||||
ItemListingDialogs(
|
||||
dialog = state.dialog,
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
ItemListingAction.DialogDismiss,
|
||||
)
|
||||
}
|
||||
},
|
||||
onConfirmDeleteClick = remember(viewModel) {
|
||||
{ itemId ->
|
||||
viewModel.trySendAction(
|
||||
ItemListingAction.ConfirmDeleteClick(itemId = itemId),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -175,10 +195,26 @@ fun ItemListingScreen(
|
||||
timeLeftSeconds = it.timeLeftSeconds,
|
||||
alertThresholdSeconds = it.alertThresholdSeconds,
|
||||
startIcon = it.startIcon,
|
||||
onItemClick = {
|
||||
viewModel.trySendAction(
|
||||
ItemListingAction.ItemClick(it.authCode)
|
||||
)
|
||||
onItemClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
ItemListingAction.ItemClick(it.id)
|
||||
)
|
||||
}
|
||||
},
|
||||
onEditItemClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
ItemListingAction.ItemClick(it.id)
|
||||
)
|
||||
}
|
||||
},
|
||||
onDeleteItemClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
ItemListingAction.DeleteItemClick(it.id)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -206,34 +242,53 @@ fun ItemListingScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (val dialog = state.dialog) {
|
||||
ItemListingState.DialogState.Syncing -> {
|
||||
BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(
|
||||
text = R.string.syncing.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is ItemListingState.DialogState.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = dialog.title,
|
||||
message = dialog.message,
|
||||
),
|
||||
onDismissRequest = {
|
||||
viewModel.trySendAction(ItemListingAction.DialogDismiss)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ItemListingDialogs(
|
||||
dialog: ItemListingState.DialogState?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onConfirmDeleteClick: (itemId: String) -> Unit,
|
||||
) {
|
||||
when (dialog) {
|
||||
ItemListingState.DialogState.Loading -> {
|
||||
BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(
|
||||
text = R.string.syncing.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is ItemListingState.DialogState.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = dialog.title,
|
||||
message = dialog.message,
|
||||
),
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
is ItemListingState.DialogState.DeleteConfirmationPrompt -> {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = R.string.delete),
|
||||
message = dialog.message(),
|
||||
confirmButtonText = stringResource(id = R.string.ok),
|
||||
dismissButtonText = stringResource(id = R.string.cancel),
|
||||
onConfirmClick = {
|
||||
onConfirmDeleteClick(dialog.itemId)
|
||||
},
|
||||
onDismissClick = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the item listing screen with no existing items.
|
||||
*/
|
||||
|
||||
@@ -10,16 +10,17 @@ import com.x8bit.bitwarden.authenticator.data.authenticator.datasource.disk.enti
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.repository.model.CreateItemResult
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.repository.model.DeleteItemResult
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem
|
||||
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util.toViewState
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.concat
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.model.IconData
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -81,6 +82,14 @@ class ItemListingViewModel @Inject constructor(
|
||||
sendEvent(ItemListingEvent.NavigateBack)
|
||||
}
|
||||
|
||||
is ItemListingAction.DeleteItemClick -> {
|
||||
handleDeleteItemClick(action)
|
||||
}
|
||||
|
||||
is ItemListingAction.ConfirmDeleteClick -> {
|
||||
handleConfirmDeleteClick(action)
|
||||
}
|
||||
|
||||
is ItemListingAction.SearchClick -> {
|
||||
sendEvent(ItemListingEvent.NavigateToSearch)
|
||||
}
|
||||
@@ -108,6 +117,33 @@ class ItemListingViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleDeleteItemClick(action: ItemListingAction.DeleteItemClick) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = ItemListingState.DialogState.DeleteConfirmationPrompt(
|
||||
message = R.string.do_you_really_want_to_permanently_delete_cipher.asText(),
|
||||
itemId = action.itemId,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConfirmDeleteClick(action: ItemListingAction.ConfirmDeleteClick) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = ItemListingState.DialogState.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
ItemListingAction.Internal.DeleteItemReceive(
|
||||
authenticatorRepository.hardDeleteItem(action.itemId)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternalAction(internalAction: ItemListingAction.Internal) {
|
||||
when (internalAction) {
|
||||
is ItemListingAction.Internal.AuthCodesUpdated -> {
|
||||
@@ -125,6 +161,36 @@ class ItemListingViewModel @Inject constructor(
|
||||
is ItemListingAction.Internal.CreateItemResultReceive -> {
|
||||
handleCreateItemResultReceive(internalAction)
|
||||
}
|
||||
|
||||
is ItemListingAction.Internal.DeleteItemReceive -> {
|
||||
handleDeleteItemReceive(internalAction.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeleteItemReceive(result: DeleteItemResult) {
|
||||
when (result) {
|
||||
DeleteItemResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = ItemListingState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DeleteItemResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = null)
|
||||
}
|
||||
sendEvent(
|
||||
ItemListingEvent.ShowToast(
|
||||
message = R.string.item_deleted.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +308,7 @@ class ItemListingViewModel @Inject constructor(
|
||||
) {
|
||||
updateStateWithVerificationCodeItems(
|
||||
authenticatorData = authenticatorData.data,
|
||||
clearDialogState = true
|
||||
clearDialogState = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -401,10 +467,10 @@ data class ItemListingState(
|
||||
sealed class DialogState : Parcelable {
|
||||
|
||||
/**
|
||||
* Displays the syncing dialog to the user.
|
||||
* Displays the loading dialog to the user.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Syncing : DialogState()
|
||||
data object Loading : DialogState()
|
||||
|
||||
/**
|
||||
* Displays a generic error dialog to the user.
|
||||
@@ -414,6 +480,12 @@ data class ItemListingState(
|
||||
val title: Text,
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
|
||||
@Parcelize
|
||||
data class DeleteConfirmationPrompt(
|
||||
val message: Text,
|
||||
val itemId: String,
|
||||
) : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,21 +597,20 @@ sealed class ItemListingAction {
|
||||
* Indicates a result for creating and item has been received.
|
||||
*/
|
||||
data class CreateItemResultReceive(val result: CreateItemResult) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The data for the verification code item to display.
|
||||
*/
|
||||
@Parcelize
|
||||
data class VerificationCodeDisplayItem(
|
||||
val id: String,
|
||||
val label: String,
|
||||
val issuer: String?,
|
||||
val supportingLabel: String?,
|
||||
val timeLeftSeconds: Int,
|
||||
val periodSeconds: Int,
|
||||
val alertThresholdSeconds: Int,
|
||||
val authCode: String,
|
||||
val startIcon: IconData = IconData.Local(R.drawable.ic_login_item),
|
||||
) : Parcelable
|
||||
/**
|
||||
* Indicates a result for deleting an item has been received.
|
||||
*/
|
||||
data class DeleteItemReceive(val result: DeleteItemResult) : Internal()
|
||||
}
|
||||
|
||||
/**
|
||||
* The user clicked Delete.
|
||||
*/
|
||||
data class DeleteItemClick(val itemId: String) : ItemListingAction()
|
||||
|
||||
/**
|
||||
* The user clicked confirm when prompted to delete an item.
|
||||
*/
|
||||
data class ConfirmDeleteClick(val itemId: String) : ItemListingAction()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -9,12 +10,21 @@ import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -36,6 +46,7 @@ import com.x8bit.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
* @param modifier The modifier for the item.
|
||||
* @param supportingLabel The supporting label for the item.
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
@Composable
|
||||
fun VaultVerificationCodeItem(
|
||||
@@ -46,15 +57,21 @@ fun VaultVerificationCodeItem(
|
||||
alertThresholdSeconds: Int,
|
||||
startIcon: IconData,
|
||||
onItemClick: () -> Unit,
|
||||
onEditItemClick: () -> Unit,
|
||||
onDeleteItemClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
supportingLabel: String? = null,
|
||||
) {
|
||||
var shouldShowDropdownMenu by remember { mutableStateOf(value = false) }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
.combinedClickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
|
||||
onClick = onItemClick,
|
||||
onLongClick = {
|
||||
shouldShowDropdownMenu = true
|
||||
}
|
||||
)
|
||||
.defaultMinSize(minHeight = 72.dp)
|
||||
.padding(vertical = 8.dp)
|
||||
@@ -107,6 +124,43 @@ fun VaultVerificationCodeItem(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = shouldShowDropdownMenu,
|
||||
onDismissRequest = { shouldShowDropdownMenu = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringResource(id = R.string.edit_item))
|
||||
},
|
||||
onClick = {
|
||||
shouldShowDropdownMenu = false
|
||||
onEditItemClick()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_edit_item),
|
||||
contentDescription = stringResource(R.string.edit_item)
|
||||
)
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringResource(id = R.string.delete_item))
|
||||
},
|
||||
onClick = {
|
||||
shouldShowDropdownMenu = false
|
||||
onDeleteItemClick()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_delete_item),
|
||||
contentDescription = stringResource(id = R.string.delete_item),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@@ -122,8 +176,10 @@ private fun VerificationCodeItem_preview() {
|
||||
alertThresholdSeconds = 7,
|
||||
startIcon = IconData.Local(R.drawable.ic_login_item),
|
||||
onItemClick = {},
|
||||
onEditItemClick = {},
|
||||
onDeleteItemClick = {},
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
supportingLabel = "Supporting Label"
|
||||
supportingLabel = "Supporting Label",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.x8bit.bitwarden.authenticator.R
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.model.IconData
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* The data for the verification code item to display.
|
||||
*/
|
||||
@Parcelize
|
||||
data class VerificationCodeDisplayItem(
|
||||
val id: String,
|
||||
val label: String,
|
||||
val issuer: String?,
|
||||
val supportingLabel: String?,
|
||||
val timeLeftSeconds: Int,
|
||||
val periodSeconds: Int,
|
||||
val alertThresholdSeconds: Int,
|
||||
val authCode: String,
|
||||
val startIcon: IconData = IconData.Local(R.drawable.ic_login_item),
|
||||
) : Parcelable
|
||||
@@ -2,7 +2,7 @@ package com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.u
|
||||
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ItemListingState
|
||||
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.VerificationCodeDisplayItem
|
||||
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem
|
||||
|
||||
fun List<VerificationCodeItem>.toViewState(
|
||||
alertThresholdSeconds: Int,
|
||||
|
||||
10
app/src/main/res/drawable/ic_delete_item.xml
Normal file
10
app/src/main/res/drawable/ic_delete_item.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M15,3V4H20V6H19V19C19,20.1 18.1,21 17,21H7C5.9,21 5,20.1 5,19V6H4V4H9V3H15ZM7,19H17V6H7V19ZM9,8H11V17H9V8ZM15,8H13V17H15V8Z"
|
||||
android:fillColor="#41484D"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_edit_item.xml
Normal file
10
app/src/main/res/drawable/ic_edit_item.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M19.06,3.59L20.41,4.94C21.2,5.72 21.2,6.99 20.41,7.77L7.18,21H3V16.82L13.4,6.41L16.23,3.59C17.01,2.81 18.28,2.81 19.06,3.59ZM5,19L6.41,19.06L16.23,9.23L14.82,7.82L5,17.64V19Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
@@ -87,4 +87,8 @@
|
||||
<string name="help">Help</string>
|
||||
<string name="tutorial">Tutorial</string>
|
||||
<string name="value_has_been_copied">%1$s copied</string>
|
||||
<string name="delete_item">Delete item</string>
|
||||
<string name="item_deleted">Item deleted</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="do_you_really_want_to_permanently_delete_cipher">Do you really want to permanently delete? This cannot be undone.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user