refactor: Clean-up history / email relays / url override lists screens

This commit is contained in:
Artem Chepurnoy 2024-01-09 21:29:38 +02:00
parent 7945402f63
commit 349e0d306f
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
8 changed files with 311 additions and 225 deletions

View File

@ -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)),
)
}
}

View File

@ -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;

View File

@ -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

View File

@ -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,

View File

@ -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,
),
),
)

View File

@ -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)),
)
}
}

View File

@ -29,6 +29,8 @@ fun DropdownScope.DropdownMenuItemFlat(
when (action) {
is ContextItem.Section -> {
Section(
modifier = Modifier
.widthIn(max = DropdownMinWidth),
text = action.title,
)
}

View File

@ -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>