feat: Offer to remove origin items during a merge
This commit is contained in:
parent
324abebb88
commit
50448c195a
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user