feat: Offer to remove origin items during a merge

This commit is contained in:
Artem Chepurnoy 2024-02-18 20:35:56 +02:00
parent 324abebb88
commit 50448c195a
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
6 changed files with 142 additions and 4 deletions

View File

@ -12,6 +12,7 @@ import kotlinx.datetime.Instant
data class CreateRequest(
val ownership: Ownership? = null,
val ownership2: Ownership2? = null,
val merge: Merge? = null,
val title: String? = null,
val note: String? = null,
val favorite: Boolean? = null,
@ -50,6 +51,14 @@ data class CreateRequest(
companion object;
}
@optics
data class Merge(
val ciphers: List<DSecret>,
val removeOrigin: Boolean,
) {
companion object;
}
@optics
sealed interface Attachment {
companion object;

View File

@ -115,6 +115,7 @@ import com.artemchep.keyguard.ui.FlatItem
import com.artemchep.keyguard.ui.FlatItemAction
import com.artemchep.keyguard.ui.FlatItemLayout
import com.artemchep.keyguard.ui.FlatItemTextContent
import com.artemchep.keyguard.ui.FlatSimpleNote
import com.artemchep.keyguard.ui.FlatTextField
import com.artemchep.keyguard.ui.FlatTextFieldBadge
import com.artemchep.keyguard.ui.LeMOdelBottomSheet
@ -124,6 +125,7 @@ import com.artemchep.keyguard.ui.PasswordFlatTextField
import com.artemchep.keyguard.ui.PasswordPwnedBadge
import com.artemchep.keyguard.ui.PasswordStrengthBadge
import com.artemchep.keyguard.ui.ScaffoldColumn
import com.artemchep.keyguard.ui.SimpleNote
import com.artemchep.keyguard.ui.UrlFlatTextField
import com.artemchep.keyguard.ui.button.FavouriteToggleButton
import com.artemchep.keyguard.ui.icons.IconBox
@ -391,6 +393,34 @@ private fun ColumnScope.populateItemsContent(
onClick = state.ownership.onClick,
)
Section()
if (state.merge != null) {
ExpandedIfNotEmpty(
valueOrNull = state.merge.note,
) { note ->
FlatSimpleNote(
modifier = Modifier,
note = note,
)
}
FlatItemLayout(
leading = {
Checkbox(
checked = state.merge.removeOrigin.checked,
onCheckedChange = null,
)
},
content = {
Text(
text = stringResource(Res.strings.additem_merge_remove_origin_ciphers_title),
)
},
onClick = {
val newValue = !state.merge.removeOrigin.checked
state.merge.removeOrigin.onChange?.invoke(newValue)
},
)
Section()
}
Spacer(Modifier.height(24.dp))
val logRepository by rememberInstance<LogRepository>()
remember(state) {

View File

@ -18,6 +18,7 @@ import com.artemchep.keyguard.feature.auth.common.TextFieldModel2
import com.artemchep.keyguard.feature.confirmation.organization.FolderInfo
import com.artemchep.keyguard.feature.filepicker.FilePickerIntent
import com.artemchep.keyguard.ui.FlatItemAction
import com.artemchep.keyguard.ui.SimpleNote
import com.artemchep.keyguard.ui.icons.AccentColors
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow
@ -30,6 +31,7 @@ data class AddState(
val title: String = "",
val favourite: SwitchFieldModel,
val ownership: Ownership,
val merge: Merge? = null,
val sideEffects: SideEffects,
val actions: List<FlatItemAction> = emptyList(),
val items: List<AddStateItem> = emptyList(),
@ -58,6 +60,12 @@ data class AddState(
)
}
data class Merge(
val ciphers: List<DSecret>,
val note: SimpleNote?,
val removeOrigin: SwitchFieldModel,
)
data class SaveToElement(
val readOnly: Boolean,
val items: List<Item> = emptyList(),

View File

@ -38,6 +38,7 @@ import com.artemchep.keyguard.common.model.Loadable
import com.artemchep.keyguard.common.model.ToastMessage
import com.artemchep.keyguard.common.model.TotpToken
import com.artemchep.keyguard.common.model.UsernameVariation2
import com.artemchep.keyguard.common.model.canDelete
import com.artemchep.keyguard.common.model.create.CreateRequest
import com.artemchep.keyguard.common.model.create.address1
import com.artemchep.keyguard.common.model.create.address2
@ -128,6 +129,7 @@ import com.artemchep.keyguard.platform.parcelize.LeParcelize
import com.artemchep.keyguard.provider.bitwarden.usecase.autofill
import com.artemchep.keyguard.res.Res
import com.artemchep.keyguard.ui.FlatItemAction
import com.artemchep.keyguard.ui.SimpleNote
import com.artemchep.keyguard.ui.icons.ChevronIcon
import com.artemchep.keyguard.ui.icons.IconBox
import com.artemchep.keyguard.ui.icons.Stub
@ -237,6 +239,39 @@ fun produceAddScreenState(
getCiphers = getCiphers,
)
val mergeFlow = if (args.merge != null) {
val ciphersHaveAttachments = args.merge.ciphers.any { it.attachments.isNotEmpty() }
val note = when {
ciphersHaveAttachments -> {
val text = translate(Res.strings.additem_merge_attachments_note)
SimpleNote(
text = text,
type = SimpleNote.Type.INFO,
)
}
else -> null
}
val mergeRemoveCiphers = mutablePersistedFlow("merge.remove_ciphers") {
false
}
mergeRemoveCiphers
.map { removeCiphers ->
val removeOrigin = SwitchFieldModel(
checked = removeCiphers,
onChange = mergeRemoveCiphers::value::set,
)
AddState.Merge(
ciphers = args.merge.ciphers,
note = note,
removeOrigin = removeOrigin,
)
}
} else {
flowOf(null)
}
val loginHolder = produceLoginState(
args = args,
ownershipFlow = ownershipFlow,
@ -685,7 +720,9 @@ fun produceAddScreenState(
}
}
val title = if (args.ownershipRo) {
val title = if (args.merge != null) {
translate(Res.strings.additem_header_merge_title)
} else if (args.ownershipRo) {
translate(Res.strings.additem_header_edit_title)
} else {
translate(Res.strings.additem_header_new_title)
@ -742,7 +779,23 @@ fun produceAddScreenState(
}
f
}
(populatorFlows.map { it.second } + ownershipPopulator + favouritePopulator + typePopulator)
val mergePopulator =
mergeFlow
.map { merge ->
val f = fun(r: CreateRequest): CreateRequest {
if (merge == null) {
return r
}
val requestMerge = CreateRequest.Merge(
ciphers = merge.ciphers,
removeOrigin = merge.removeOrigin.checked,
)
return r.copy(merge = requestMerge)
}
f
}
(populatorFlows.map { it.second } + ownershipPopulator + mergePopulator + favouritePopulator + typePopulator)
.combineToList()
}
.map { populators ->
@ -756,10 +809,11 @@ fun produceAddScreenState(
val f = combine(
actions,
favouriteFlow,
ownershipFlow,
ownershipFlow
.combine(mergeFlow) { a, b -> a to b },
itfff,
outputFlow,
) { q, s, c, x, request ->
) { q, s, (c, merge), x, request ->
logRepository.post(
"Foo3",
"create state ${x.size} (+${items1.size}) items| ${x.joinToString { it.id }}",
@ -768,6 +822,7 @@ fun produceAddScreenState(
title = title,
favourite = s,
ownership = c,
merge = merge,
actions = q,
items = items1 + x,
sideEffects = sideEffects,

View File

@ -6,16 +6,20 @@ import com.artemchep.keyguard.common.io.bind
import com.artemchep.keyguard.common.io.combine
import com.artemchep.keyguard.common.io.effectMap
import com.artemchep.keyguard.common.io.flatMap
import com.artemchep.keyguard.common.io.flatTap
import com.artemchep.keyguard.common.io.io
import com.artemchep.keyguard.common.io.ioEffect
import com.artemchep.keyguard.common.io.ioUnit
import com.artemchep.keyguard.common.io.map
import com.artemchep.keyguard.common.model.AccountId
import com.artemchep.keyguard.common.model.DSecret
import com.artemchep.keyguard.common.model.canDelete
import com.artemchep.keyguard.common.model.create.CreateRequest
import com.artemchep.keyguard.common.service.crypto.CryptoGenerator
import com.artemchep.keyguard.common.usecase.AddCipher
import com.artemchep.keyguard.common.usecase.AddFolder
import com.artemchep.keyguard.common.usecase.GetPasswordStrength
import com.artemchep.keyguard.common.usecase.TrashCipherById
import com.artemchep.keyguard.core.store.bitwarden.BitwardenCipher
import com.artemchep.keyguard.core.store.bitwarden.BitwardenService
import com.artemchep.keyguard.feature.confirmation.organization.FolderInfo
@ -32,6 +36,7 @@ import org.kodein.di.instance
class AddCipherImpl(
private val modifyDatabase: ModifyDatabase,
private val addFolder: AddFolder,
private val trashCipherById: TrashCipherById,
private val cryptoGenerator: CryptoGenerator,
private val getPasswordStrength: GetPasswordStrength,
) : AddCipher {
@ -44,6 +49,7 @@ class AddCipherImpl(
constructor(directDI: DirectDI) : this(
modifyDatabase = directDI.instance(),
addFolder = directDI.instance(),
trashCipherById = directDI.instance(),
cryptoGenerator = directDI.instance(),
getPasswordStrength = directDI.instance(),
)
@ -132,6 +138,33 @@ class AddCipherImpl(
.map { it.cipherId },
)
}
}.flatTap {
val cipherIdsToTrash = cipherIdsToRequests
.values
.asSequence()
.mapNotNull {
val ciphers = it.merge?.ciphers
?: return@mapNotNull null
// Ignore the request if we do not need to trash the
// origin ciphers.
if (!it.merge.removeOrigin) {
return@mapNotNull null
}
ciphers
}
.flatten()
.filter { cipher ->
cipher.canDelete() &&
!cipher.deleted
}
.map { cipher ->
cipher.id
}
.toSet()
if (cipherIdsToTrash.isEmpty()) {
return@flatTap ioUnit()
}
trashCipherById(cipherIdsToTrash)
}
private fun copyLocalFilesToInternalStorageIo(

View File

@ -560,6 +560,9 @@
<string name="additem_header_new_title">New item</string>
<string name="additem_header_edit_title">Edit item</string>
<string name="additem_header_merge_title">Merge items</string>
<string name="additem_merge_remove_origin_ciphers_title">Move origin items to trash</string>
<string name="additem_merge_attachments_note">Merging items does not merge their attachments! The created item will not have any attachments added.</string>
<string name="additem_markdown_note">Style text with <xliff:g id="italic" example="italic">%1$s</xliff:g> or <xliff:g id="bold" example="bold">%2$s</xliff:g> and more. Limited Markdown syntax is supported.</string>
<string name="additem_markdown_note_italic">italic</string>
<string name="additem_markdown_note_bold">bold</string>