refactor: Clean-up history / email relays / url override lists screens
This commit is contained in:
parent
7945402f63
commit
349e0d306f
|
@ -165,19 +165,25 @@ fun produceEmailRelayListState(
|
|||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
fun onDelete(
|
||||
emailRelayIds: Set<String>,
|
||||
fun onDeleteByItems(
|
||||
items: List<DGeneratorEmailRelay>,
|
||||
) {
|
||||
val title = if (emailRelayIds.size > 1) {
|
||||
val title = if (items.size > 1) {
|
||||
translate(Res.strings.emailrelay_delete_many_confirmation_title)
|
||||
} else {
|
||||
translate(Res.strings.emailrelay_delete_one_confirmation_title)
|
||||
}
|
||||
val message = items
|
||||
.joinToString(separator = "\n") { it.name }
|
||||
val intent = createConfirmationDialogIntent(
|
||||
icon = icon(Icons.Outlined.Delete),
|
||||
title = title,
|
||||
message = message,
|
||||
) {
|
||||
removeEmailRelayById(emailRelayIds)
|
||||
val ids = items
|
||||
.mapNotNull { it.id }
|
||||
.toSet()
|
||||
removeEmailRelayById(ids)
|
||||
.launchIn(appScope)
|
||||
}
|
||||
navigate(intent)
|
||||
|
@ -227,15 +233,16 @@ fun produceEmailRelayListState(
|
|||
return@map null
|
||||
}
|
||||
|
||||
val actions = mutableListOf<FlatItemAction>()
|
||||
actions += FlatItemAction(
|
||||
leading = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.delete),
|
||||
onClick = {
|
||||
val ids = selectedItems.mapNotNull { it.id }.toSet()
|
||||
onDelete(ids)
|
||||
},
|
||||
)
|
||||
val actions = buildContextItems {
|
||||
section {
|
||||
this += FlatItemAction(
|
||||
leading = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.delete),
|
||||
onClick = ::onDeleteByItems
|
||||
.partially1(selectedItems),
|
||||
)
|
||||
}
|
||||
}
|
||||
Selection(
|
||||
count = selectedItems.size,
|
||||
actions = actions.toPersistentList(),
|
||||
|
@ -278,8 +285,8 @@ fun produceEmailRelayListState(
|
|||
this += FlatItemAction(
|
||||
icon = Icons.Outlined.Delete,
|
||||
title = translate(Res.strings.delete),
|
||||
onClick = ::onDelete
|
||||
.partially1(setOfNotNull(it.id)),
|
||||
onClick = ::onDeleteByItems
|
||||
.partially1(listOf(it)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.artemchep.keyguard.feature.generator.history
|
|||
import androidx.compose.runtime.Immutable
|
||||
import arrow.optics.optics
|
||||
import com.artemchep.keyguard.feature.attachments.SelectableItemState
|
||||
import com.artemchep.keyguard.ui.ContextItem
|
||||
import com.artemchep.keyguard.ui.FlatItemAction
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
@ -36,7 +37,7 @@ sealed interface GeneratorHistoryItem {
|
|||
* List of the callable actions appended
|
||||
* to the item.
|
||||
*/
|
||||
val dropdown: PersistentList<FlatItemAction>,
|
||||
val dropdown: PersistentList<ContextItem>,
|
||||
val selectableState: StateFlow<SelectableItemState>,
|
||||
) : GeneratorHistoryItem {
|
||||
companion object;
|
||||
|
|
|
@ -222,7 +222,10 @@ private fun GeneratorHistoryItem(
|
|||
trailing = {
|
||||
val onCopyAction = remember(item.dropdown) {
|
||||
item.dropdown
|
||||
.firstOrNull { it.type == FlatItemAction.Type.COPY }
|
||||
.firstNotNullOfOrNull {
|
||||
val action = it as? FlatItemAction
|
||||
action?.takeIf { it.type == FlatItemAction.Type.COPY }
|
||||
}
|
||||
}
|
||||
if (onCopyAction != null) {
|
||||
val onCopy = onCopyAction.onClick
|
||||
|
|
|
@ -5,8 +5,10 @@ import androidx.compose.material.icons.outlined.Delete
|
|||
import androidx.compose.runtime.Composable
|
||||
import arrow.core.partially1
|
||||
import com.artemchep.keyguard.common.io.launchIn
|
||||
import com.artemchep.keyguard.common.model.DGeneratorHistory
|
||||
import com.artemchep.keyguard.common.model.Loadable
|
||||
import com.artemchep.keyguard.common.service.clipboard.ClipboardService
|
||||
import com.artemchep.keyguard.common.usecase.CopyText
|
||||
import com.artemchep.keyguard.common.usecase.DateFormatter
|
||||
import com.artemchep.keyguard.common.usecase.GetGeneratorHistory
|
||||
import com.artemchep.keyguard.common.usecase.RemoveGeneratorHistory
|
||||
|
@ -15,17 +17,16 @@ import com.artemchep.keyguard.common.util.flow.persistingStateIn
|
|||
import com.artemchep.keyguard.feature.attachments.SelectableItemState
|
||||
import com.artemchep.keyguard.feature.attachments.SelectableItemStateRaw
|
||||
import com.artemchep.keyguard.feature.auth.common.util.REGEX_EMAIL
|
||||
import com.artemchep.keyguard.feature.confirmation.ConfirmationResult
|
||||
import com.artemchep.keyguard.feature.confirmation.ConfirmationRoute
|
||||
import com.artemchep.keyguard.feature.confirmation.createConfirmationDialogIntent
|
||||
import com.artemchep.keyguard.feature.decorator.ItemDecoratorDate
|
||||
import com.artemchep.keyguard.feature.largetype.LargeTypeRoute
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
import com.artemchep.keyguard.feature.navigation.registerRouteResultReceiver
|
||||
import com.artemchep.keyguard.feature.navigation.state.copy
|
||||
import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
||||
import com.artemchep.keyguard.feature.passwordleak.PasswordLeakRoute
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import com.artemchep.keyguard.ui.FlatItemAction
|
||||
import com.artemchep.keyguard.ui.Selection
|
||||
import com.artemchep.keyguard.ui.buildContextItems
|
||||
import com.artemchep.keyguard.ui.icons.icon
|
||||
import com.artemchep.keyguard.ui.selection.selectionHandle
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
@ -79,63 +80,64 @@ fun produceGeneratorHistoryState(
|
|||
),
|
||||
) {
|
||||
val selectionHandle = selectionHandle("selection")
|
||||
|
||||
val copyFactory = copy(clipboardService)
|
||||
|
||||
val itemsRawFlow = getGeneratorHistory()
|
||||
.shareInScreenScope()
|
||||
val optionsFlow = itemsRawFlow
|
||||
.map { items ->
|
||||
items.isEmpty()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.map { isEmpty ->
|
||||
if (isEmpty) {
|
||||
persistentListOf()
|
||||
} else {
|
||||
val action = FlatItemAction(
|
||||
leading = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.generatorhistory_clear_history_title),
|
||||
onClick = {
|
||||
val route = registerRouteResultReceiver(
|
||||
route = ConfirmationRoute(
|
||||
args = ConfirmationRoute.Args(
|
||||
icon = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.generatorhistory_clear_history_confirmation_title),
|
||||
message = translate(Res.strings.generatorhistory_clear_history_confirmation_text),
|
||||
),
|
||||
),
|
||||
) {
|
||||
if (it is ConfirmationResult.Confirm) {
|
||||
removeGeneratorHistory()
|
||||
.launchIn(appScope)
|
||||
}
|
||||
}
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
persistentListOf(action)
|
||||
}
|
||||
}
|
||||
// Automatically de-select items
|
||||
// that do not exist.
|
||||
combine(
|
||||
itemsRawFlow,
|
||||
selectionHandle.idsFlow,
|
||||
) { items, selectedItemIds ->
|
||||
val newSelectedAttachmentIds = selectedItemIds
|
||||
val newSelectedItemIds = selectedItemIds
|
||||
.asSequence()
|
||||
.filter { attachmentId ->
|
||||
items.any { it.id == attachmentId }
|
||||
.filter { itemId ->
|
||||
items.any { it.id == itemId }
|
||||
}
|
||||
.toSet()
|
||||
newSelectedAttachmentIds.takeIf { it.size < selectedItemIds.size }
|
||||
newSelectedItemIds.takeIf { it.size < selectedItemIds.size }
|
||||
}
|
||||
.filterNotNull()
|
||||
.onEach { ids -> selectionHandle.setSelection(ids) }
|
||||
.launchIn(screenScope)
|
||||
|
||||
fun onDeleteByItems(
|
||||
items: List<DGeneratorHistory>,
|
||||
) {
|
||||
val title = if (items.size > 1) {
|
||||
translate(Res.strings.generatorhistory_delete_many_confirmation_title)
|
||||
} else {
|
||||
translate(Res.strings.generatorhistory_delete_one_confirmation_title)
|
||||
}
|
||||
val message = items
|
||||
.joinToString(separator = "\n") { it.value }
|
||||
val intent = createConfirmationDialogIntent(
|
||||
icon = icon(Icons.Outlined.Delete),
|
||||
title = title,
|
||||
message = message,
|
||||
) {
|
||||
val ids = items
|
||||
.mapNotNull { it.id }
|
||||
.toSet()
|
||||
removeGeneratorHistoryById(ids)
|
||||
.launchIn(appScope)
|
||||
}
|
||||
navigate(intent)
|
||||
}
|
||||
|
||||
fun onDeleteAll() {
|
||||
val intent = createConfirmationDialogIntent(
|
||||
icon = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.generatorhistory_clear_history_confirmation_title),
|
||||
message = translate(Res.strings.generatorhistory_clear_history_confirmation_text),
|
||||
) {
|
||||
removeGeneratorHistory()
|
||||
.launchIn(appScope)
|
||||
}
|
||||
navigate(intent)
|
||||
}
|
||||
|
||||
val selectionFlow = combine(
|
||||
itemsRawFlow,
|
||||
selectionHandle.idsFlow,
|
||||
|
@ -149,19 +151,19 @@ fun produceGeneratorHistoryState(
|
|||
return@map null
|
||||
}
|
||||
|
||||
val actions = mutableListOf<FlatItemAction>()
|
||||
actions += FlatItemAction(
|
||||
leading = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.remove_from_history),
|
||||
onClick = {
|
||||
val ids = selectedItems.mapNotNull { it.id }.toSet()
|
||||
removeGeneratorHistoryById(ids)
|
||||
.launchIn(appScope)
|
||||
},
|
||||
)
|
||||
val actions = buildContextItems {
|
||||
section {
|
||||
this += FlatItemAction(
|
||||
leading = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.remove_from_history),
|
||||
onClick = ::onDeleteByItems
|
||||
.partially1(selectedItems),
|
||||
)
|
||||
}
|
||||
}
|
||||
Selection(
|
||||
count = selectedItems.size,
|
||||
actions = actions.toPersistentList(),
|
||||
actions = actions,
|
||||
onSelectAll = if (selectedItems.size < allItems.size) {
|
||||
val allIds = allItems
|
||||
.asSequence()
|
||||
|
@ -197,25 +199,60 @@ fun produceGeneratorHistoryState(
|
|||
|
||||
else -> null
|
||||
}
|
||||
val actions = listOfNotNull(
|
||||
copyFactory.FlatItemAction(
|
||||
title = translate(Res.strings.copy_value),
|
||||
value = item.value,
|
||||
hidden = item.isPassword,
|
||||
),
|
||||
LargeTypeRoute.showInLargeTypeActionOrNull(
|
||||
translator = this@produceScreenState,
|
||||
text = item.value,
|
||||
colorize = item.isPassword,
|
||||
navigate = ::navigate,
|
||||
),
|
||||
LargeTypeRoute.showInLargeTypeActionAndLockOrNull(
|
||||
translator = this@produceScreenState,
|
||||
text = item.value,
|
||||
colorize = item.isPassword,
|
||||
navigate = ::navigate,
|
||||
),
|
||||
).toPersistentList()
|
||||
val actions = buildContextItems {
|
||||
section {
|
||||
val (copyTitle, copyType) = when (type) {
|
||||
GeneratorHistoryItem.Value.Type.PASSWORD ->
|
||||
translate(Res.strings.copy_password) to CopyText.Type.PASSWORD
|
||||
GeneratorHistoryItem.Value.Type.EMAIL,
|
||||
GeneratorHistoryItem.Value.Type.EMAIL_RELAY ->
|
||||
translate(Res.strings.copy_email) to CopyText.Type.EMAIL
|
||||
GeneratorHistoryItem.Value.Type.USERNAME ->
|
||||
translate(Res.strings.copy_username) to CopyText.Type.USERNAME
|
||||
null -> translate(Res.strings.copy_value) to CopyText.Type.VALUE
|
||||
}
|
||||
this += copyFactory.FlatItemAction(
|
||||
title = copyTitle,
|
||||
value = item.value,
|
||||
hidden = item.isPassword,
|
||||
type = copyType,
|
||||
)
|
||||
val items = listOfNotNull(
|
||||
item,
|
||||
)
|
||||
this += FlatItemAction(
|
||||
leading = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.remove_from_history),
|
||||
onClick = ::onDeleteByItems
|
||||
.partially1(items),
|
||||
)
|
||||
}
|
||||
section {
|
||||
this += LargeTypeRoute.showInLargeTypeActionOrNull(
|
||||
translator = this@produceScreenState,
|
||||
text = item.value,
|
||||
colorize = item.isPassword,
|
||||
navigate = ::navigate,
|
||||
)
|
||||
this += LargeTypeRoute.showInLargeTypeActionAndLockOrNull(
|
||||
translator = this@produceScreenState,
|
||||
text = item.value,
|
||||
colorize = item.isPassword,
|
||||
navigate = ::navigate,
|
||||
)
|
||||
}
|
||||
section {
|
||||
// If the value type is a password, then offer to
|
||||
// check it in the breaches.
|
||||
if (type == GeneratorHistoryItem.Value.Type.PASSWORD) {
|
||||
this += PasswordLeakRoute.checkBreachesPasswordAction(
|
||||
translator = this@produceScreenState,
|
||||
password = item.value,
|
||||
navigate = ::navigate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val selectableFlow = selectionHandle
|
||||
.idsFlow
|
||||
.map { selectedIds ->
|
||||
|
@ -285,6 +322,23 @@ fun produceGeneratorHistoryState(
|
|||
}
|
||||
}.toPersistentList()
|
||||
}
|
||||
val optionsFlow = itemsRawFlow
|
||||
.map { items ->
|
||||
items.isEmpty()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.map { isEmpty ->
|
||||
if (isEmpty) {
|
||||
persistentListOf()
|
||||
} else {
|
||||
val action = FlatItemAction(
|
||||
leading = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.generatorhistory_clear_history_title),
|
||||
onClick = ::onDeleteAll,
|
||||
)
|
||||
persistentListOf(action)
|
||||
}
|
||||
}
|
||||
combine(
|
||||
optionsFlow,
|
||||
selectionFlow,
|
||||
|
|
|
@ -9,16 +9,14 @@ import com.artemchep.keyguard.common.model.DSecret
|
|||
import com.artemchep.keyguard.common.service.clipboard.ClipboardService
|
||||
import com.artemchep.keyguard.common.usecase.CipherRemovePasswordHistory
|
||||
import com.artemchep.keyguard.common.usecase.CipherRemovePasswordHistoryById
|
||||
import com.artemchep.keyguard.common.usecase.CopyText
|
||||
import com.artemchep.keyguard.common.usecase.DateFormatter
|
||||
import com.artemchep.keyguard.common.usecase.GetAccounts
|
||||
import com.artemchep.keyguard.common.usecase.GetCanWrite
|
||||
import com.artemchep.keyguard.common.usecase.GetCiphers
|
||||
import com.artemchep.keyguard.feature.confirmation.ConfirmationResult
|
||||
import com.artemchep.keyguard.feature.confirmation.ConfirmationRoute
|
||||
import com.artemchep.keyguard.feature.confirmation.createConfirmationDialogIntent
|
||||
import com.artemchep.keyguard.feature.home.vault.model.VaultPasswordHistoryItem
|
||||
import com.artemchep.keyguard.feature.largetype.LargeTypeRoute
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
import com.artemchep.keyguard.feature.navigation.registerRouteResultReceiver
|
||||
import com.artemchep.keyguard.feature.navigation.state.copy
|
||||
import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
||||
import com.artemchep.keyguard.feature.passwordleak.PasswordLeakRoute
|
||||
|
@ -28,10 +26,12 @@ import com.artemchep.keyguard.ui.FlatItemAction
|
|||
import com.artemchep.keyguard.ui.Selection
|
||||
import com.artemchep.keyguard.ui.buildContextItems
|
||||
import com.artemchep.keyguard.ui.icons.icon
|
||||
import com.artemchep.keyguard.ui.selection.selectionHandle
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
@ -79,9 +79,9 @@ fun vaultViewPasswordHistoryScreenState(
|
|||
itemId,
|
||||
),
|
||||
) {
|
||||
val copy = copy(
|
||||
clipboardService = clipboardService,
|
||||
)
|
||||
val selectionHandle = selectionHandle("selection")
|
||||
val copyFactory = copy(clipboardService)
|
||||
|
||||
val secretFlow = getCiphers()
|
||||
.map { secrets ->
|
||||
secrets
|
||||
|
@ -89,88 +89,7 @@ fun vaultViewPasswordHistoryScreenState(
|
|||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
val selectionSink = mutablePersistedFlow("selection") {
|
||||
listOf<String>()
|
||||
}
|
||||
// Automatically remove selection from items
|
||||
// that do not exist anymore.
|
||||
secretFlow
|
||||
.onEach { secretOrNull ->
|
||||
val f = secretOrNull?.login?.passwordHistory.orEmpty()
|
||||
val selectedAccountIds = selectionSink.value
|
||||
val filteredSelectedAccountIds = selectedAccountIds
|
||||
.filter { id ->
|
||||
f.any { it.id == id }
|
||||
}
|
||||
if (filteredSelectedAccountIds.size < selectedAccountIds.size) {
|
||||
selectionSink.value = filteredSelectedAccountIds
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
fun clearSelection() {
|
||||
selectionSink.value = emptyList()
|
||||
}
|
||||
|
||||
fun toggleSelection(entry: DSecret.Login.PasswordHistory) {
|
||||
val entryId = entry.id
|
||||
|
||||
val oldAccountIds = selectionSink.value
|
||||
val newAccountIds =
|
||||
if (entryId in oldAccountIds) {
|
||||
oldAccountIds - entryId
|
||||
} else {
|
||||
oldAccountIds + entryId
|
||||
}
|
||||
selectionSink.value = newAccountIds
|
||||
}
|
||||
|
||||
val selectionFlow = combine(
|
||||
selectionSink,
|
||||
getCanWrite(),
|
||||
) { ids, canWrite ->
|
||||
if (ids.isEmpty()) {
|
||||
return@combine null
|
||||
}
|
||||
|
||||
val actions = if (canWrite) {
|
||||
val removeAction = FlatItemAction(
|
||||
icon = Icons.Outlined.Delete,
|
||||
title = translate(Res.strings.remove_from_history),
|
||||
onClick = {
|
||||
val route = registerRouteResultReceiver(
|
||||
route = ConfirmationRoute(
|
||||
args = ConfirmationRoute.Args(
|
||||
icon = icon(Icons.Outlined.Delete),
|
||||
title = "Remove ${ids.size} passwords from the history?",
|
||||
),
|
||||
),
|
||||
) {
|
||||
if (it is ConfirmationResult.Confirm) {
|
||||
cipherRemovePasswordHistoryById(
|
||||
itemId,
|
||||
ids,
|
||||
).launchIn(appScope)
|
||||
}
|
||||
}
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
persistentListOf(
|
||||
removeAction,
|
||||
)
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
Selection(
|
||||
count = ids.size,
|
||||
actions = actions,
|
||||
onClear = ::clearSelection,
|
||||
)
|
||||
}
|
||||
|
||||
val itemsFlow = secretFlow
|
||||
val itemsRawFlow = secretFlow
|
||||
.map { secretOrNull ->
|
||||
secretOrNull
|
||||
?.login
|
||||
|
@ -178,6 +97,104 @@ fun vaultViewPasswordHistoryScreenState(
|
|||
.orEmpty()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.shareInScreenScope()
|
||||
// Automatically de-select items
|
||||
// that do not exist.
|
||||
combine(
|
||||
itemsRawFlow,
|
||||
selectionHandle.idsFlow,
|
||||
) { items, selectedItemIds ->
|
||||
val newSelectedItemIds = selectedItemIds
|
||||
.asSequence()
|
||||
.filter { itemId ->
|
||||
items.any { it.id == itemId }
|
||||
}
|
||||
.toSet()
|
||||
newSelectedItemIds.takeIf { it.size < selectedItemIds.size }
|
||||
}
|
||||
.filterNotNull()
|
||||
.onEach { ids -> selectionHandle.setSelection(ids) }
|
||||
.launchIn(screenScope)
|
||||
|
||||
fun onDeleteByItems(
|
||||
items: List<DSecret.Login.PasswordHistory>,
|
||||
) {
|
||||
val title = if (items.size > 1) {
|
||||
translate(Res.strings.passwordhistory_delete_many_confirmation_title)
|
||||
} else {
|
||||
translate(Res.strings.passwordhistory_delete_one_confirmation_title)
|
||||
}
|
||||
val message = items
|
||||
.joinToString(separator = "\n") { it.password }
|
||||
val intent = createConfirmationDialogIntent(
|
||||
icon = icon(Icons.Outlined.Delete),
|
||||
title = title,
|
||||
message = message,
|
||||
) {
|
||||
val ids = items
|
||||
.map { it.id }
|
||||
cipherRemovePasswordHistoryById(
|
||||
itemId,
|
||||
ids,
|
||||
).launchIn(appScope)
|
||||
}
|
||||
navigate(intent)
|
||||
}
|
||||
|
||||
fun onDeleteAll() {
|
||||
val intent = createConfirmationDialogIntent(
|
||||
icon = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.passwordhistory_clear_history_confirmation_title),
|
||||
message = translate(Res.strings.passwordhistory_clear_history_confirmation_text),
|
||||
) {
|
||||
cipherRemovePasswordHistory(
|
||||
itemId,
|
||||
).launchIn(appScope)
|
||||
}
|
||||
navigate(intent)
|
||||
}
|
||||
|
||||
val selectionFlow = combine(
|
||||
itemsRawFlow,
|
||||
selectionHandle.idsFlow,
|
||||
) { items, selectedItemIds ->
|
||||
val selectedItems = items
|
||||
.filter { it.id in selectedItemIds }
|
||||
items to selectedItems
|
||||
}
|
||||
.combine(getCanWrite()) { (allItems, selectedItems), canWrite ->
|
||||
if (selectedItems.isEmpty()) {
|
||||
return@combine null
|
||||
}
|
||||
|
||||
val actions = buildContextItems {
|
||||
section {
|
||||
this += FlatItemAction(
|
||||
leading = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.remove_from_history),
|
||||
onClick = ::onDeleteByItems
|
||||
.partially1(selectedItems),
|
||||
)
|
||||
}
|
||||
}
|
||||
Selection(
|
||||
count = selectedItems.size,
|
||||
actions = actions,
|
||||
onSelectAll = if (selectedItems.size < allItems.size) {
|
||||
val allIds = allItems
|
||||
.asSequence()
|
||||
.mapNotNull { it.id }
|
||||
.toSet()
|
||||
selectionHandle::setSelection
|
||||
.partially1(allIds)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onClear = selectionHandle::clearSelection,
|
||||
)
|
||||
}
|
||||
|
||||
val itemsFlow = itemsRawFlow
|
||||
.map { passwordHistory ->
|
||||
passwordHistory
|
||||
.sortedByDescending { it.lastUsedDate }
|
||||
|
@ -187,9 +204,19 @@ fun vaultViewPasswordHistoryScreenState(
|
|||
date = dateFormatter.formatDateTime(password.lastUsedDate),
|
||||
actions = buildContextItems {
|
||||
section {
|
||||
this += copy.FlatItemAction(
|
||||
this += copyFactory.FlatItemAction(
|
||||
title = translate(Res.strings.copy_password),
|
||||
value = password.password,
|
||||
type = CopyText.Type.PASSWORD,
|
||||
)
|
||||
val items = listOf(
|
||||
password,
|
||||
)
|
||||
this += FlatItemAction(
|
||||
leading = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.remove_from_history),
|
||||
onClick = ::onDeleteByItems
|
||||
.partially1(items),
|
||||
)
|
||||
}
|
||||
section {
|
||||
|
@ -217,14 +244,15 @@ fun vaultViewPasswordHistoryScreenState(
|
|||
)
|
||||
}
|
||||
}
|
||||
.combine(selectionSink) { passwords, ids ->
|
||||
.combine(selectionHandle.idsFlow) { passwords, ids ->
|
||||
val selectionMode = ids.isNotEmpty()
|
||||
passwords
|
||||
.asSequence()
|
||||
.map { passwordWrapper ->
|
||||
val password = passwordWrapper.src
|
||||
val selected = password.id in ids
|
||||
val onToggle = ::toggleSelection.partially1(password)
|
||||
val onToggle = selectionHandle::toggleSelection
|
||||
.partially1(password.id)
|
||||
VaultPasswordHistoryItem.Value(
|
||||
id = password.id,
|
||||
title = passwordWrapper.date,
|
||||
|
@ -245,25 +273,7 @@ fun vaultViewPasswordHistoryScreenState(
|
|||
FlatItemAction(
|
||||
icon = Icons.Outlined.Delete,
|
||||
title = translate(Res.strings.passwordhistory_clear_history_title),
|
||||
onClick = {
|
||||
val route = registerRouteResultReceiver(
|
||||
route = ConfirmationRoute(
|
||||
args = ConfirmationRoute.Args(
|
||||
icon = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.passwordhistory_clear_history_confirmation_title),
|
||||
message = translate(Res.strings.passwordhistory_clear_history_confirmation_text),
|
||||
),
|
||||
),
|
||||
) {
|
||||
if (it is ConfirmationResult.Confirm) {
|
||||
cipherRemovePasswordHistory(
|
||||
itemId,
|
||||
).launchIn(appScope)
|
||||
}
|
||||
}
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
onClick = ::onDeleteAll,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -27,8 +27,6 @@ import com.artemchep.keyguard.feature.home.vault.model.VaultItemIcon
|
|||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
import com.artemchep.keyguard.feature.navigation.registerRouteResultReceiver
|
||||
import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
||||
import com.artemchep.keyguard.platform.CurrentPlatform
|
||||
import com.artemchep.keyguard.platform.Platform
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import com.artemchep.keyguard.ui.FlatItemAction
|
||||
import com.artemchep.keyguard.ui.Selection
|
||||
|
@ -169,19 +167,25 @@ fun produceUrlOverrideListState(
|
|||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
fun onDelete(
|
||||
emailRelayIds: Set<String>,
|
||||
fun onDeleteByItems(
|
||||
items: List<DGlobalUrlOverride>,
|
||||
) {
|
||||
val title = if (emailRelayIds.size > 1) {
|
||||
val title = if (items.size > 1) {
|
||||
translate(Res.strings.urloverride_delete_many_confirmation_title)
|
||||
} else {
|
||||
translate(Res.strings.urloverride_delete_one_confirmation_title)
|
||||
}
|
||||
val message = items
|
||||
.joinToString(separator = "\n") { it.name }
|
||||
val intent = createConfirmationDialogIntent(
|
||||
icon = icon(Icons.Outlined.Delete),
|
||||
title = title,
|
||||
message = message,
|
||||
) {
|
||||
removeUrlOverrideById(emailRelayIds)
|
||||
val ids = items
|
||||
.mapNotNull { it.id }
|
||||
.toSet()
|
||||
removeUrlOverrideById(ids)
|
||||
.launchIn(appScope)
|
||||
}
|
||||
navigate(intent)
|
||||
|
@ -218,18 +222,19 @@ fun produceUrlOverrideListState(
|
|||
return@map null
|
||||
}
|
||||
|
||||
val actions = mutableListOf<FlatItemAction>()
|
||||
actions += FlatItemAction(
|
||||
leading = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.delete),
|
||||
onClick = {
|
||||
val ids = selectedItems.mapNotNull { it.id }.toSet()
|
||||
onDelete(ids)
|
||||
},
|
||||
)
|
||||
val actions = buildContextItems {
|
||||
section {
|
||||
this += FlatItemAction(
|
||||
leading = icon(Icons.Outlined.Delete),
|
||||
title = translate(Res.strings.delete),
|
||||
onClick = ::onDeleteByItems
|
||||
.partially1(selectedItems),
|
||||
)
|
||||
}
|
||||
}
|
||||
Selection(
|
||||
count = selectedItems.size,
|
||||
actions = actions.toPersistentList(),
|
||||
actions = actions,
|
||||
onSelectAll = if (selectedItems.size < allItems.size) {
|
||||
val allIds = allItems
|
||||
.asSequence()
|
||||
|
@ -264,8 +269,8 @@ fun produceUrlOverrideListState(
|
|||
this += FlatItemAction(
|
||||
icon = Icons.Outlined.Delete,
|
||||
title = translate(Res.strings.delete),
|
||||
onClick = ::onDelete
|
||||
.partially1(setOfNotNull(it.id)),
|
||||
onClick = ::onDeleteByItems
|
||||
.partially1(listOf(it)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,8 @@ fun DropdownScope.DropdownMenuItemFlat(
|
|||
when (action) {
|
||||
is ContextItem.Section -> {
|
||||
Section(
|
||||
modifier = Modifier
|
||||
.widthIn(max = DropdownMinWidth),
|
||||
text = action.title,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -206,8 +206,8 @@
|
|||
|
||||
<string name="folder_action_change_name_title">Change name</string>
|
||||
<string name="folder_action_change_names_title">Change names</string>
|
||||
<string name="folder_delete_one_confirmation_title">Delete folder?</string>
|
||||
<string name="folder_delete_many_confirmation_title">Delete folders?</string>
|
||||
<string name="folder_delete_one_confirmation_title">Delete the folder?</string>
|
||||
<string name="folder_delete_many_confirmation_title">Delete the folders?</string>
|
||||
<string name="folder_delete_confirmation_text">This item(s) will be deleted immediately. You can not undo this action.</string>
|
||||
|
||||
<string name="ciphers_recently_opened">Recently opened</string>
|
||||
|
@ -470,8 +470,8 @@
|
|||
|
||||
<string name="emailrelay_list_header_title">Email forwarders</string>
|
||||
<string name="emailrelay_list_section_title">Email forwarders</string>
|
||||
<string name="emailrelay_delete_one_confirmation_title">Delete email forwarder?</string>
|
||||
<string name="emailrelay_delete_many_confirmation_title">Delete email forwarders?</string>
|
||||
<string name="emailrelay_delete_one_confirmation_title">Delete the email forwarder?</string>
|
||||
<string name="emailrelay_delete_many_confirmation_title">Delete the email forwarders?</string>
|
||||
<string name="emailrelay_integration_title">Email forwarder integration</string>
|
||||
<string name="emailrelay_empty_label">No email forwarders</string>
|
||||
|
||||
|
@ -479,8 +479,8 @@
|
|||
<string name="urloverride_list_header_title">URL overrides</string>
|
||||
<string name="urloverride_list_section_title">URL overrides</string>
|
||||
<string name="urloverride_regex_note">The override will be applied to URLs that match the regular expression.</string>
|
||||
<string name="urloverride_delete_one_confirmation_title">Delete URL override?</string>
|
||||
<string name="urloverride_delete_many_confirmation_title">Delete URL overrides?</string>
|
||||
<string name="urloverride_delete_one_confirmation_title">Delete the URL override?</string>
|
||||
<string name="urloverride_delete_many_confirmation_title">Delete the URL overrides?</string>
|
||||
<string name="urloverride_empty_label">No URL overrides</string>
|
||||
|
||||
<string name="setup_header_text">Create an encrypted vault where the local data will be stored.</string>
|
||||
|
@ -720,11 +720,15 @@
|
|||
<string name="generatorhistory_clear_history_title">Clear history</string>
|
||||
<string name="generatorhistory_clear_history_confirmation_title">Clear generator history?</string>
|
||||
<string name="generatorhistory_clear_history_confirmation_text">This will remove all items from the history.</string>
|
||||
<string name="generatorhistory_delete_one_confirmation_title">Delete the item from the history?</string>
|
||||
<string name="generatorhistory_delete_many_confirmation_title">Delete the items from the history?</string>
|
||||
|
||||
<string name="passwordhistory_header_title">Password history</string>
|
||||
<string name="passwordhistory_clear_history_title">Clear history</string>
|
||||
<string name="passwordhistory_clear_history_confirmation_title">Clear password history?</string>
|
||||
<string name="passwordhistory_clear_history_confirmation_text">This will remove all passwords from the history.</string>
|
||||
<string name="passwordhistory_delete_one_confirmation_title">Delete the password from the history?</string>
|
||||
<string name="passwordhistory_delete_many_confirmation_title">Delete the passwords from the history?</string>
|
||||
|
||||
<string name="watchtower_header_title">Watchtower</string>
|
||||
<string name="watchtower_section_password_strength_label">Password strength</string>
|
||||
|
|
Loading…
Reference in New Issue