feat: Basic Sends editing capabilities
This commit is contained in:
parent
91e130674a
commit
d00781636d
@ -36,7 +36,8 @@ data class DSend(
|
||||
val notes: String,
|
||||
val accessCount: Int,
|
||||
val maxAccessCount: Int? = null,
|
||||
val password: String? = null,
|
||||
val hasPassword: Boolean,
|
||||
val synced: Boolean,
|
||||
val disabled: Boolean,
|
||||
val hideEmail: Boolean,
|
||||
val type: Type,
|
||||
|
@ -0,0 +1,20 @@
|
||||
package com.artemchep.keyguard.common.model
|
||||
|
||||
import arrow.core.None
|
||||
import arrow.core.Option
|
||||
|
||||
data class PatchSendRequest(
|
||||
val patch: Map<String, Data>,
|
||||
) {
|
||||
data class Data(
|
||||
val name: Option<String> = None,
|
||||
val hideEmail: Option<Boolean> = None,
|
||||
val disabled: Option<Boolean> = None,
|
||||
val password: Option<String?> = None,
|
||||
/**
|
||||
* Changes the file name of the send.
|
||||
* Note: only applied to sends with a file type.
|
||||
*/
|
||||
val fileName: Option<String> = None,
|
||||
)
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package com.artemchep.keyguard.common.model.create
|
||||
|
||||
import arrow.optics.optics
|
||||
import com.artemchep.keyguard.common.model.DSecret
|
||||
import com.artemchep.keyguard.common.model.DSend
|
||||
import com.artemchep.keyguard.feature.confirmation.organization.FolderInfo
|
||||
import com.artemchep.keyguard.platform.LeUri
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@optics
|
||||
data class CreateSendRequest(
|
||||
val ownership: Ownership? = null,
|
||||
val title: String? = null,
|
||||
val note: String? = null,
|
||||
val favorite: Boolean? = null,
|
||||
val reprompt: Boolean? = null,
|
||||
val uris: PersistentList<DSecret.Uri> = persistentListOf(),
|
||||
val fido2Credentials: PersistentList<DSecret.Login.Fido2Credentials> = persistentListOf(),
|
||||
val fields: PersistentList<DSecret.Field> = persistentListOf(),
|
||||
// types
|
||||
val type: DSend.Type? = null,
|
||||
val text: Text = Text(),
|
||||
val file: File = File(),
|
||||
// other
|
||||
val now: Instant,
|
||||
) {
|
||||
companion object;
|
||||
|
||||
@optics
|
||||
data class Ownership(
|
||||
val accountId: String?,
|
||||
) {
|
||||
companion object;
|
||||
}
|
||||
|
||||
@optics
|
||||
data class File(
|
||||
val text: String? = null,
|
||||
) {
|
||||
companion object;
|
||||
}
|
||||
|
||||
@optics
|
||||
data class Text(
|
||||
val text: String? = null,
|
||||
val hidden: Boolean? = null,
|
||||
) {
|
||||
companion object;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.artemchep.keyguard.common.usecase
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
import com.artemchep.keyguard.common.model.create.CreateRequest
|
||||
import com.artemchep.keyguard.common.model.create.CreateSendRequest
|
||||
|
||||
interface AddSend : (
|
||||
Map<String?, CreateSendRequest>,
|
||||
) -> IO<List<String>>
|
@ -0,0 +1,8 @@
|
||||
package com.artemchep.keyguard.common.usecase
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
import com.artemchep.keyguard.common.model.PatchSendRequest
|
||||
|
||||
interface PatchSendById : (
|
||||
PatchSendRequest,
|
||||
) -> IO<Boolean>
|
@ -0,0 +1,7 @@
|
||||
package com.artemchep.keyguard.common.usecase
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
|
||||
interface RemoveSendById : (
|
||||
Set<String>,
|
||||
) -> IO<Unit>
|
@ -0,0 +1,19 @@
|
||||
package com.artemchep.keyguard.common.usecase
|
||||
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
interface SendToolbox {
|
||||
val patchSendById: PatchSendById
|
||||
val removeSendById: RemoveSendById
|
||||
}
|
||||
|
||||
class SendToolboxImpl(
|
||||
override val patchSendById: PatchSendById,
|
||||
override val removeSendById: RemoveSendById,
|
||||
) : SendToolbox {
|
||||
constructor(directDI: DirectDI) : this(
|
||||
patchSendById = directDI.instance(),
|
||||
removeSendById = directDI.instance(),
|
||||
)
|
||||
}
|
@ -10,6 +10,7 @@ import com.artemchep.keyguard.common.usecase.GetAccounts
|
||||
import com.artemchep.keyguard.common.usecase.GetCiphers
|
||||
import com.artemchep.keyguard.common.usecase.GetFolders
|
||||
import com.artemchep.keyguard.common.usecase.GetMetas
|
||||
import com.artemchep.keyguard.common.usecase.GetSends
|
||||
import com.artemchep.keyguard.common.util.flow.combineToList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@ -24,6 +25,7 @@ class GetAccountStatusImpl(
|
||||
private val getMetas: GetMetas,
|
||||
private val getCiphers: GetCiphers,
|
||||
private val getFolders: GetFolders,
|
||||
private val getSends: GetSends,
|
||||
) : GetAccountStatus {
|
||||
private val importantPermissions = listOf(
|
||||
Permission.POST_NOTIFICATIONS,
|
||||
@ -35,6 +37,7 @@ class GetAccountStatusImpl(
|
||||
getMetas = directDI.instance(),
|
||||
getCiphers = directDI.instance(),
|
||||
getFolders = directDI.instance(),
|
||||
getSends = directDI.instance(),
|
||||
)
|
||||
|
||||
override fun invoke(): Flow<DAccountStatus> {
|
||||
@ -65,7 +68,11 @@ class GetAccountStatusImpl(
|
||||
.map {
|
||||
it.count { !it.synced }
|
||||
}
|
||||
combine(c, f) { a, b -> a + b }
|
||||
val s = getSends()
|
||||
.map {
|
||||
it.count { !it.synced }
|
||||
}
|
||||
combine(c, f, s) { a, b, s -> a + b + s }
|
||||
}
|
||||
|
||||
val pendingPermissionsFlow = importantPermissions
|
||||
|
@ -26,12 +26,15 @@ class GetEnvSendUrlImpl(
|
||||
): IO<String> = tokenRepository
|
||||
.getById(AccountId(send.accountId))
|
||||
.effectMap { token ->
|
||||
val sendBaseUrl = token?.env?.back()?.buildSendUrl()
|
||||
requireNotNull(sendBaseUrl) { "Failed to get base send url." }
|
||||
val baseUrl = token?.env?.back()?.buildSendUrl()
|
||||
requireNotNull(baseUrl) { "Failed to get base send url." }
|
||||
|
||||
val accessId = send.accessId.takeIf { it.isNotEmpty() }
|
||||
requireNotNull(accessId) { "Failed to get access id." }
|
||||
|
||||
val sendUrl = buildString {
|
||||
append(sendBaseUrl)
|
||||
append(send.accessId)
|
||||
append(baseUrl)
|
||||
append(accessId)
|
||||
append('/')
|
||||
|
||||
val key = send.keyBase64
|
||||
|
@ -44,6 +44,7 @@ import com.artemchep.keyguard.common.usecase.AddEmailRelay
|
||||
import com.artemchep.keyguard.common.usecase.AddFolder
|
||||
import com.artemchep.keyguard.common.usecase.AddGeneratorHistory
|
||||
import com.artemchep.keyguard.common.usecase.AddPasskeyCipher
|
||||
import com.artemchep.keyguard.common.usecase.AddSend
|
||||
import com.artemchep.keyguard.common.usecase.AddUriCipher
|
||||
import com.artemchep.keyguard.common.usecase.AddUrlOverride
|
||||
import com.artemchep.keyguard.common.usecase.ChangeCipherNameById
|
||||
@ -94,6 +95,7 @@ import com.artemchep.keyguard.common.usecase.GetUrlOverrides
|
||||
import com.artemchep.keyguard.common.usecase.GetWordlistPrimitive
|
||||
import com.artemchep.keyguard.common.usecase.MergeFolderById
|
||||
import com.artemchep.keyguard.common.usecase.MoveCipherToFolderById
|
||||
import com.artemchep.keyguard.common.usecase.PatchSendById
|
||||
import com.artemchep.keyguard.common.usecase.PatchWatchtowerAlertCipher
|
||||
import com.artemchep.keyguard.common.usecase.PutAccountColorById
|
||||
import com.artemchep.keyguard.common.usecase.PutAccountMasterPasswordHintById
|
||||
@ -108,11 +110,14 @@ import com.artemchep.keyguard.common.usecase.RemoveEmailRelayById
|
||||
import com.artemchep.keyguard.common.usecase.RemoveFolderById
|
||||
import com.artemchep.keyguard.common.usecase.RemoveGeneratorHistory
|
||||
import com.artemchep.keyguard.common.usecase.RemoveGeneratorHistoryById
|
||||
import com.artemchep.keyguard.common.usecase.RemoveSendById
|
||||
import com.artemchep.keyguard.common.usecase.RemoveUrlOverrideById
|
||||
import com.artemchep.keyguard.common.usecase.RenameFolderById
|
||||
import com.artemchep.keyguard.common.usecase.RestoreCipherById
|
||||
import com.artemchep.keyguard.common.usecase.RetryCipher
|
||||
import com.artemchep.keyguard.common.usecase.RotateDeviceIdUseCase
|
||||
import com.artemchep.keyguard.common.usecase.SendToolbox
|
||||
import com.artemchep.keyguard.common.usecase.SendToolboxImpl
|
||||
import com.artemchep.keyguard.common.usecase.SupervisorRead
|
||||
import com.artemchep.keyguard.common.usecase.SyncAll
|
||||
import com.artemchep.keyguard.common.usecase.SyncById
|
||||
@ -156,6 +161,7 @@ import com.artemchep.keyguard.provider.bitwarden.usecase.AddCipherUsedAutofillHi
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.AddCipherUsedPasskeyHistoryImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.AddFolderImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.AddPasskeyCipherImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.AddSendImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.AddUriCipherImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.ChangeCipherNameByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.ChangeCipherPasswordByIdImpl
|
||||
@ -196,6 +202,7 @@ import com.artemchep.keyguard.provider.bitwarden.usecase.GetUrlOverridesImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.GetWordlistPrimitiveImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.MergeFolderByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.MoveCipherToFolderByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.PatchSendByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.PatchWatchtowerAlertCipherImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.PutAccountColorByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.PutAccountMasterPasswordHintByIdImpl
|
||||
@ -208,6 +215,7 @@ import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveCipherByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveWordlistByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveEmailRelayByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveFolderByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveSendByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RemoveUrlOverrideByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RenameFolderByIdImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.RestoreCipherByIdImpl
|
||||
@ -226,6 +234,7 @@ import com.artemchep.keyguard.provider.bitwarden.usecase.util.ModifyCipherById
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.util.ModifyDatabase
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.util.ModifyFolderById
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.util.ModifyProfileById
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.util.ModifySendById
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.bindSingleton
|
||||
import org.kodein.di.instance
|
||||
@ -341,6 +350,9 @@ fun DI.Builder.createSubDi2(
|
||||
bindSingleton<ModifyCipherById> {
|
||||
ModifyCipherById(this)
|
||||
}
|
||||
bindSingleton<ModifySendById> {
|
||||
ModifySendById(this)
|
||||
}
|
||||
bindSingleton<ModifyFolderById> {
|
||||
ModifyFolderById(this)
|
||||
}
|
||||
@ -365,6 +377,12 @@ fun DI.Builder.createSubDi2(
|
||||
bindSingleton<RemoveCipherById> {
|
||||
RemoveCipherByIdImpl(this)
|
||||
}
|
||||
bindSingleton<RemoveSendById> {
|
||||
RemoveSendByIdImpl(this)
|
||||
}
|
||||
bindSingleton<PatchSendById> {
|
||||
PatchSendByIdImpl(this)
|
||||
}
|
||||
bindSingleton<RemoveFolderById> {
|
||||
RemoveFolderByIdImpl(this)
|
||||
}
|
||||
@ -389,6 +407,9 @@ fun DI.Builder.createSubDi2(
|
||||
bindSingleton<CipherToolbox> {
|
||||
CipherToolboxImpl(this)
|
||||
}
|
||||
bindSingleton<SendToolbox> {
|
||||
SendToolboxImpl(this)
|
||||
}
|
||||
bindSingleton<CipherUnsecureUrlCheck> {
|
||||
CipherUnsecureUrlCheckImpl(this)
|
||||
}
|
||||
@ -460,6 +481,9 @@ fun DI.Builder.createSubDi2(
|
||||
bindSingleton<AddCipher> {
|
||||
AddCipherImpl(this)
|
||||
}
|
||||
bindSingleton<AddSend> {
|
||||
AddSendImpl(this)
|
||||
}
|
||||
bindSingleton<AddCipherOpenedHistory> {
|
||||
AddCipherOpenedHistoryImpl(this)
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
package com.artemchep.keyguard.core.store.bitwarden
|
||||
|
||||
import arrow.optics.optics
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@optics
|
||||
@Serializable
|
||||
sealed interface BitwardenOptionalStringNullable {
|
||||
companion object;
|
||||
|
||||
@optics
|
||||
@Serializable
|
||||
@SerialName("some")
|
||||
data class Some(
|
||||
val value: String?,
|
||||
) : BitwardenOptionalStringNullable {
|
||||
companion object;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("none")
|
||||
data object None : BitwardenOptionalStringNullable
|
||||
}
|
@ -2,6 +2,7 @@ package com.artemchep.keyguard.core.store.bitwarden
|
||||
|
||||
import arrow.optics.optics
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@ -33,6 +34,8 @@ data class BitwardenSend(
|
||||
val type: Type = Type.None,
|
||||
val file: File? = null,
|
||||
val text: Text? = null,
|
||||
// changes
|
||||
val changes: Changes? = null,
|
||||
) : BitwardenService.Has<BitwardenSend> {
|
||||
companion object;
|
||||
|
||||
@ -45,6 +48,15 @@ data class BitwardenSend(
|
||||
Text,
|
||||
}
|
||||
|
||||
@optics
|
||||
@Serializable
|
||||
@SerialName("changes")
|
||||
data class Changes(
|
||||
val passwordBase64: BitwardenOptionalStringNullable = BitwardenOptionalStringNullable.None,
|
||||
) {
|
||||
companion object;
|
||||
}
|
||||
|
||||
//
|
||||
// Types
|
||||
//
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,32 @@
|
||||
package com.artemchep.keyguard.feature.add
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import com.artemchep.keyguard.ui.focus.FocusRequester2
|
||||
|
||||
class AddScreenScope(
|
||||
initialFocusRequested: Boolean = false,
|
||||
) {
|
||||
val initialFocusRequestedState = mutableStateOf(initialFocusRequested)
|
||||
|
||||
@Composable
|
||||
fun initialFocusRequesterEffect(): FocusRequester2 {
|
||||
val focusRequester = remember { FocusRequester2() }
|
||||
// Auto focus the text field
|
||||
// on launch.
|
||||
LaunchedEffect(focusRequester) {
|
||||
var initialFocusRequested by initialFocusRequestedState
|
||||
if (!initialFocusRequested) {
|
||||
focusRequester.requestFocus()
|
||||
// do not request it the second time
|
||||
initialFocusRequested = true
|
||||
}
|
||||
}
|
||||
return focusRequester
|
||||
}
|
||||
}
|
@ -0,0 +1,299 @@
|
||||
package com.artemchep.keyguard.feature.add
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import com.artemchep.keyguard.common.model.DSecret
|
||||
import com.artemchep.keyguard.common.model.TotpToken
|
||||
import com.artemchep.keyguard.common.model.UsernameVariation2
|
||||
import com.artemchep.keyguard.common.model.create.CreateRequest
|
||||
import com.artemchep.keyguard.common.usecase.CopyText
|
||||
import com.artemchep.keyguard.feature.auth.common.SwitchFieldModel
|
||||
import com.artemchep.keyguard.feature.auth.common.TextFieldModel2
|
||||
import com.artemchep.keyguard.ui.ContextItem
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalTime
|
||||
|
||||
sealed interface AddStateItem {
|
||||
val id: String
|
||||
|
||||
interface HasOptions<T> {
|
||||
val options: ImmutableList<ContextItem>
|
||||
|
||||
/**
|
||||
* Copies the data class replacing the old options with a
|
||||
* provided ones.
|
||||
*/
|
||||
fun withOptions(
|
||||
options: ImmutableList<ContextItem>,
|
||||
): T
|
||||
}
|
||||
|
||||
interface HasState<T, Request> {
|
||||
val state: LocalStateItem<T, Request>
|
||||
}
|
||||
|
||||
@Stable
|
||||
data class Title<Request>(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<TextFieldModel2, Request>,
|
||||
) : AddStateItem, HasState<TextFieldModel2, Request>
|
||||
|
||||
@Stable
|
||||
data class Username<Request>(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<State, Request>,
|
||||
) : AddStateItem, HasState<Username.State, Request> {
|
||||
data class State(
|
||||
val value: TextFieldModel2,
|
||||
val type: UsernameVariation2,
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
data class Password<Request>(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<TextFieldModel2, Request>,
|
||||
) : AddStateItem, HasState<TextFieldModel2, Request>
|
||||
|
||||
@Stable
|
||||
data class Text<Request>(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<State, Request>,
|
||||
) : AddStateItem, HasState<Text.State, Request> {
|
||||
data class State(
|
||||
val value: TextFieldModel2,
|
||||
val label: String? = null,
|
||||
val singleLine: Boolean = false,
|
||||
val keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
val visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
)
|
||||
}
|
||||
|
||||
data class Suggestion<Request>(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<State, Request>,
|
||||
) : AddStateItem, HasState<Suggestion.State, Request> {
|
||||
data class State(
|
||||
val items: ImmutableList<Item>,
|
||||
)
|
||||
|
||||
data class Item(
|
||||
val key: String,
|
||||
val text: String,
|
||||
val value: String,
|
||||
val source: String,
|
||||
val selected: Boolean,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
)
|
||||
}
|
||||
|
||||
data class Totp<Request>(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<State, Request>,
|
||||
) : AddStateItem, HasState<Totp.State, Request> {
|
||||
data class State(
|
||||
val copyText: CopyText,
|
||||
val value: TextFieldModel2,
|
||||
val totpToken: TotpToken? = null,
|
||||
)
|
||||
}
|
||||
|
||||
data class Passkey<Request>(
|
||||
override val id: String,
|
||||
override val options: ImmutableList<ContextItem> = persistentListOf(),
|
||||
override val state: LocalStateItem<State, Request>,
|
||||
) : AddStateItem, HasOptions<Passkey<*>>, HasState<Passkey.State, Request> {
|
||||
override fun withOptions(
|
||||
options: ImmutableList<ContextItem>,
|
||||
) = copy(
|
||||
options = options,
|
||||
)
|
||||
|
||||
data class State(
|
||||
val passkey: DSecret.Login.Fido2Credentials?,
|
||||
)
|
||||
}
|
||||
|
||||
data class Attachment<Request>(
|
||||
override val id: String,
|
||||
override val options: ImmutableList<ContextItem> = persistentListOf(),
|
||||
override val state: LocalStateItem<State, Request>,
|
||||
) : AddStateItem, HasOptions<Attachment<*>>, HasState<Attachment.State, Request> {
|
||||
override fun withOptions(
|
||||
options: ImmutableList<ContextItem>,
|
||||
) = copy(
|
||||
options = options,
|
||||
)
|
||||
|
||||
data class State(
|
||||
val id: String,
|
||||
val name: TextFieldModel2,
|
||||
val size: String? = null,
|
||||
/**
|
||||
* `true` if the attachment is already uploaded to the server,
|
||||
* `false` otherwise.
|
||||
*/
|
||||
val synced: Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
data class Url<Request>(
|
||||
override val id: String,
|
||||
override val options: ImmutableList<ContextItem> = persistentListOf(),
|
||||
override val state: LocalStateItem<State, Request>,
|
||||
) : AddStateItem, HasOptions<Url<*>>, HasState<Url.State, Request> {
|
||||
override fun withOptions(
|
||||
options: ImmutableList<ContextItem>,
|
||||
) = copy(
|
||||
options = options,
|
||||
)
|
||||
|
||||
data class State(
|
||||
override val options: ImmutableList<ContextItem> = persistentListOf(),
|
||||
val text: TextFieldModel2,
|
||||
val matchType: DSecret.Uri.MatchType? = null,
|
||||
val matchTypeTitle: String? = null,
|
||||
) : HasOptions<State> {
|
||||
override fun withOptions(
|
||||
options: ImmutableList<ContextItem>,
|
||||
) = copy(
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Field<Request>(
|
||||
override val id: String,
|
||||
override val options: ImmutableList<ContextItem> = persistentListOf(),
|
||||
override val state: LocalStateItem<State, Request>,
|
||||
) : AddStateItem, HasOptions<Field<*>>, HasState<Field.State, Request> {
|
||||
override fun withOptions(
|
||||
options: ImmutableList<ContextItem>,
|
||||
) = copy(
|
||||
options = options,
|
||||
)
|
||||
|
||||
sealed interface State : HasOptions<State> {
|
||||
data class Text(
|
||||
override val options: ImmutableList<ContextItem> = persistentListOf(),
|
||||
val label: TextFieldModel2,
|
||||
val text: TextFieldModel2,
|
||||
val hidden: Boolean = false,
|
||||
) : State {
|
||||
override fun withOptions(
|
||||
options: ImmutableList<ContextItem>,
|
||||
) = copy(
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
|
||||
data class Switch(
|
||||
override val options: ImmutableList<ContextItem> = persistentListOf(),
|
||||
val checked: Boolean = false,
|
||||
val onCheckedChange: ((Boolean) -> Unit)? = null,
|
||||
val label: TextFieldModel2,
|
||||
) : State {
|
||||
override fun withOptions(
|
||||
options: ImmutableList<ContextItem>,
|
||||
) = copy(
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
|
||||
data class LinkedId(
|
||||
override val options: ImmutableList<ContextItem> = persistentListOf(),
|
||||
val value: DSecret.Field.LinkedId?,
|
||||
val actions: ImmutableList<ContextItem>,
|
||||
val label: TextFieldModel2,
|
||||
) : State {
|
||||
override fun withOptions(
|
||||
options: ImmutableList<ContextItem>,
|
||||
) = copy(
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Note<Request>(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<TextFieldModel2, Request>,
|
||||
val markdown: Boolean,
|
||||
) : AddStateItem, HasState<TextFieldModel2, Request>
|
||||
|
||||
data class Enum<Request>(
|
||||
override val id: String,
|
||||
val icon: ImageVector,
|
||||
val label: String,
|
||||
override val state: LocalStateItem<State, Request>,
|
||||
) : AddStateItem, HasState<Enum.State, Request> {
|
||||
data class State(
|
||||
val value: String = "",
|
||||
val dropdown: ImmutableList<ContextItem> = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
data class Switch<Request>(
|
||||
override val id: String,
|
||||
val title: String,
|
||||
val text: String? = null,
|
||||
override val state: LocalStateItem<SwitchFieldModel, Request>,
|
||||
) : AddStateItem, HasState<SwitchFieldModel, Request>
|
||||
|
||||
data class Section(
|
||||
override val id: String,
|
||||
val text: String? = null,
|
||||
) : AddStateItem
|
||||
|
||||
//
|
||||
// CUSTOM
|
||||
//
|
||||
|
||||
data class DateMonthYear<Request>(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<State, Request>,
|
||||
val label: String,
|
||||
) : AddStateItem, HasState<DateMonthYear.State, Request> {
|
||||
data class State(
|
||||
val month: TextFieldModel2,
|
||||
val year: TextFieldModel2,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
||||
data class DateTime<Request>(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<State, Request>,
|
||||
val label: String,
|
||||
) : AddStateItem, HasState<DateTime.State, Request> {
|
||||
data class State(
|
||||
val localDate: LocalDate,
|
||||
val localTime: LocalTime,
|
||||
val onSelectDate: () -> Unit,
|
||||
val onSelectTime: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
||||
//
|
||||
// Items that may modify the amount of items in the
|
||||
// list.
|
||||
//
|
||||
|
||||
data class Add(
|
||||
override val id: String,
|
||||
val text: String,
|
||||
val actions: ImmutableList<ContextItem>,
|
||||
) : AddStateItem
|
||||
}
|
||||
|
||||
@Stable
|
||||
data class LocalStateItem<T, Request>(
|
||||
val flow: StateFlow<T>,
|
||||
val populator: Request.(T) -> Request = { this },
|
||||
)
|
@ -0,0 +1,24 @@
|
||||
package com.artemchep.keyguard.feature.add
|
||||
|
||||
import com.artemchep.keyguard.ui.icons.AccentColors
|
||||
|
||||
data class AddStateOwnership(
|
||||
val account: Element? = null,
|
||||
val organization: Element? = null,
|
||||
val collection: Element? = null,
|
||||
val folder: Element? = null,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
data class Element(
|
||||
val readOnly: Boolean,
|
||||
val items: List<Item> = emptyList(),
|
||||
) {
|
||||
data class Item(
|
||||
val key: String,
|
||||
val title: String,
|
||||
val text: String? = null,
|
||||
val stub: Boolean = false,
|
||||
val accentColors: AccentColors? = null,
|
||||
)
|
||||
}
|
||||
}
|
@ -28,11 +28,21 @@ class OrganizationConfirmationRoute(
|
||||
private const val RAW_RO_ORGANIZATION = 2
|
||||
private const val RAW_RO_COLLECTION = 4
|
||||
private const val RAW_RO_FOLDER = 8
|
||||
private const val RAW_HIDE_ORGANIZATION = 16
|
||||
private const val RAW_HIDE_COLLECTION = 32
|
||||
private const val RAW_HIDE_FOLDER = 64
|
||||
private const val RAW_PREMIUM_ACCOUNT = 128
|
||||
|
||||
const val RO_ACCOUNT = RAW_RO_ACCOUNT
|
||||
const val RO_ORGANIZATION = RAW_RO_ORGANIZATION
|
||||
const val RO_COLLECTION = RAW_RO_COLLECTION
|
||||
const val RO_FOLDER = RAW_RO_FOLDER
|
||||
|
||||
const val HIDE_ORGANIZATION = RAW_RO_ORGANIZATION or RAW_HIDE_ORGANIZATION
|
||||
const val HIDE_COLLECTION = RAW_RO_COLLECTION or RAW_HIDE_COLLECTION
|
||||
const val HIDE_FOLDER = RAW_RO_FOLDER or RAW_HIDE_FOLDER
|
||||
|
||||
const val PREMIUM_ACCOUNT = RAW_PREMIUM_ACCOUNT
|
||||
}
|
||||
|
||||
data class Decor(
|
||||
|
@ -11,9 +11,9 @@ data class OrganizationConfirmationState(
|
||||
) {
|
||||
data class Content(
|
||||
val accounts: Section,
|
||||
val organizations: Section,
|
||||
val collections: Section,
|
||||
val folders: Section,
|
||||
val organizations: Section?,
|
||||
val collections: Section?,
|
||||
val folders: Section?,
|
||||
val folderNew: TextFieldModel2?,
|
||||
) {
|
||||
data class Section(
|
||||
|
@ -38,6 +38,7 @@ import com.artemchep.keyguard.ui.theme.isDark
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
@ -54,7 +55,6 @@ fun organizationConfirmationState(
|
||||
organizationConfirmationState(
|
||||
args = args,
|
||||
transmitter = transmitter,
|
||||
getAccounts = instance(),
|
||||
getProfiles = instance(),
|
||||
getOrganizations = instance(),
|
||||
getCollections = instance(),
|
||||
@ -192,7 +192,6 @@ private data class FolderVariant(
|
||||
fun organizationConfirmationState(
|
||||
args: OrganizationConfirmationRoute.Args,
|
||||
transmitter: RouteResultTransmitter<OrganizationConfirmationResult>,
|
||||
getAccounts: GetAccounts,
|
||||
getProfiles: GetProfiles,
|
||||
getOrganizations: GetOrganizations,
|
||||
getCollections: GetCollections,
|
||||
@ -211,11 +210,18 @@ fun organizationConfirmationState(
|
||||
val readOnlyFolder = OrganizationConfirmationRoute.Args.RO_FOLDER in args.flags
|
||||
val readOnlyCollections = OrganizationConfirmationRoute.Args.RO_COLLECTION in args.flags
|
||||
|
||||
val hideOrganization = OrganizationConfirmationRoute.Args.HIDE_ORGANIZATION in args.flags
|
||||
val hideFolder = OrganizationConfirmationRoute.Args.HIDE_FOLDER in args.flags
|
||||
val hideCollections = OrganizationConfirmationRoute.Args.HIDE_COLLECTION in args.flags
|
||||
|
||||
val premiumAccount = OrganizationConfirmationRoute.Args.PREMIUM_ACCOUNT in args.flags
|
||||
|
||||
val accountsFlow = getProfiles()
|
||||
.map { profiles ->
|
||||
profiles
|
||||
.map { profile ->
|
||||
val enabled = profile.accountId() !in args.blacklistedAccountIds
|
||||
val enabled = profile.accountId() !in args.blacklistedAccountIds &&
|
||||
(!premiumAccount || profile.premium)
|
||||
AccountVariant(
|
||||
accountId = profile.accountId(),
|
||||
name = profile.email,
|
||||
@ -494,7 +500,11 @@ fun organizationConfirmationState(
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
val itemOrganizationsFlow = combine(
|
||||
val itemOrganizationsFlow = if (hideOrganization) {
|
||||
// When we hide the organization UI we just display
|
||||
// a section as null.
|
||||
flowOf(null)
|
||||
} else combine(
|
||||
organizationsFlow,
|
||||
selectionFlow
|
||||
.map { it.organizationId to it.accountId }
|
||||
@ -527,7 +537,11 @@ fun organizationConfirmationState(
|
||||
.takeIf { selectedOrganizationId != null },
|
||||
)
|
||||
}
|
||||
val itemCollectionsFlow = combine(
|
||||
val itemCollectionsFlow = if (hideCollections) {
|
||||
// When we hide the collection UI we just display
|
||||
// a section as null.
|
||||
flowOf(null)
|
||||
} else combine(
|
||||
collectionsFlow,
|
||||
selectionFlow
|
||||
.map {
|
||||
@ -577,7 +591,11 @@ fun organizationConfirmationState(
|
||||
val accountId: String?,
|
||||
)
|
||||
|
||||
val itemFoldersFlow = combine(
|
||||
val itemFoldersFlow = if (hideFolder) {
|
||||
// When we hide the collection UI we just display
|
||||
// a section as null.
|
||||
flowOf(null)
|
||||
} else combine(
|
||||
foldersFlow,
|
||||
selectionFlow
|
||||
.map {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,5 @@
|
||||
package com.artemchep.keyguard.feature.home.vault.add
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
@ -13,6 +12,8 @@ import com.artemchep.keyguard.common.model.TotpToken
|
||||
import com.artemchep.keyguard.common.model.UsernameVariation2
|
||||
import com.artemchep.keyguard.common.model.create.CreateRequest
|
||||
import com.artemchep.keyguard.common.usecase.CopyText
|
||||
import com.artemchep.keyguard.feature.add.AddStateItem
|
||||
import com.artemchep.keyguard.feature.add.AddStateOwnership
|
||||
import com.artemchep.keyguard.feature.auth.common.SwitchFieldModel
|
||||
import com.artemchep.keyguard.feature.auth.common.TextFieldModel2
|
||||
import com.artemchep.keyguard.feature.confirmation.organization.FolderInfo
|
||||
@ -46,11 +47,7 @@ data class AddState(
|
||||
|
||||
data class Ownership(
|
||||
val data: Data,
|
||||
val account: SaveToElement? = null,
|
||||
val organization: SaveToElement? = null,
|
||||
val collection: SaveToElement? = null,
|
||||
val folder: SaveToElement? = null,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
val ui: AddStateOwnership,
|
||||
) {
|
||||
data class Data(
|
||||
val accountId: String?,
|
||||
@ -65,281 +62,4 @@ data class AddState(
|
||||
val note: SimpleNote?,
|
||||
val removeOrigin: SwitchFieldModel,
|
||||
)
|
||||
|
||||
data class SaveToElement(
|
||||
val readOnly: Boolean,
|
||||
val items: List<Item> = emptyList(),
|
||||
) {
|
||||
data class Item(
|
||||
val key: String,
|
||||
val title: String,
|
||||
val text: String? = null,
|
||||
val accentColors: AccentColors? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface AddStateItem {
|
||||
val id: String
|
||||
|
||||
interface HasOptions<T> {
|
||||
val options: List<FlatItemAction>
|
||||
|
||||
/**
|
||||
* Copies the data class replacing the old options with a
|
||||
* provided ones.
|
||||
*/
|
||||
fun withOptions(
|
||||
options: List<FlatItemAction>,
|
||||
): T
|
||||
}
|
||||
|
||||
interface HasDecor {
|
||||
val decor: Decor
|
||||
}
|
||||
|
||||
interface HasState<T> {
|
||||
val state: LocalStateItem<T>
|
||||
}
|
||||
|
||||
data class Decor(
|
||||
val shape: Shape = RectangleShape,
|
||||
val elevation: Dp = 0.dp,
|
||||
)
|
||||
|
||||
data class Title(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<TextFieldModel2>,
|
||||
) : AddStateItem, HasState<TextFieldModel2>
|
||||
|
||||
data class Username(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<State>,
|
||||
) : AddStateItem, HasState<Username.State> {
|
||||
data class State(
|
||||
val value: TextFieldModel2,
|
||||
val type: UsernameVariation2,
|
||||
)
|
||||
}
|
||||
|
||||
data class Password(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<TextFieldModel2>,
|
||||
) : AddStateItem, HasState<TextFieldModel2>
|
||||
|
||||
data class Text(
|
||||
override val id: String,
|
||||
override val decor: Decor = Decor(),
|
||||
override val state: LocalStateItem<State>,
|
||||
) : AddStateItem, HasDecor, HasState<Text.State> {
|
||||
data class State(
|
||||
val value: TextFieldModel2,
|
||||
val label: String? = null,
|
||||
val singleLine: Boolean = false,
|
||||
val keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
val visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
)
|
||||
}
|
||||
|
||||
data class Suggestion(
|
||||
override val id: String,
|
||||
override val decor: Decor = Decor(),
|
||||
override val state: LocalStateItem<State>,
|
||||
) : AddStateItem, HasDecor, HasState<Suggestion.State> {
|
||||
data class State(
|
||||
val items: ImmutableList<Item>,
|
||||
)
|
||||
|
||||
data class Item(
|
||||
val key: String,
|
||||
val text: String,
|
||||
val value: String,
|
||||
val source: String,
|
||||
val selected: Boolean,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
)
|
||||
}
|
||||
|
||||
data class Totp(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<State>,
|
||||
) : AddStateItem, HasState<Totp.State> {
|
||||
data class State(
|
||||
val copyText: CopyText,
|
||||
val value: TextFieldModel2,
|
||||
val totpToken: TotpToken? = null,
|
||||
)
|
||||
}
|
||||
|
||||
data class Passkey(
|
||||
override val id: String,
|
||||
override val options: List<FlatItemAction> = emptyList(),
|
||||
override val state: LocalStateItem<State>,
|
||||
) : AddStateItem, HasOptions<Passkey>, HasState<Passkey.State> {
|
||||
override fun withOptions(
|
||||
options: List<FlatItemAction>,
|
||||
): Passkey = copy(
|
||||
options = options,
|
||||
)
|
||||
|
||||
data class State(
|
||||
val passkey: DSecret.Login.Fido2Credentials?,
|
||||
)
|
||||
}
|
||||
|
||||
data class Attachment(
|
||||
override val id: String,
|
||||
override val options: List<FlatItemAction> = emptyList(),
|
||||
override val state: LocalStateItem<State>,
|
||||
) : AddStateItem, HasOptions<Attachment>, HasState<Attachment.State> {
|
||||
override fun withOptions(options: List<FlatItemAction>): Attachment =
|
||||
copy(
|
||||
options = options,
|
||||
)
|
||||
|
||||
data class State(
|
||||
val id: String,
|
||||
val name: TextFieldModel2,
|
||||
val size: String? = null,
|
||||
/**
|
||||
* `true` if the attachment is already uploaded to the server,
|
||||
* `false` otherwise.
|
||||
*/
|
||||
val synced: Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
data class Url(
|
||||
override val id: String,
|
||||
override val options: List<FlatItemAction> = emptyList(),
|
||||
override val state: LocalStateItem<State>,
|
||||
) : AddStateItem, HasOptions<Url>, HasState<Url.State> {
|
||||
override fun withOptions(options: List<FlatItemAction>): Url =
|
||||
copy(
|
||||
options = options,
|
||||
)
|
||||
|
||||
data class State(
|
||||
override val options: List<FlatItemAction> = emptyList(),
|
||||
val text: TextFieldModel2,
|
||||
val matchType: DSecret.Uri.MatchType? = null,
|
||||
val matchTypeTitle: String? = null,
|
||||
) : HasOptions<State> {
|
||||
override fun withOptions(options: List<FlatItemAction>): State =
|
||||
copy(
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Field(
|
||||
override val id: String,
|
||||
override val options: List<FlatItemAction> = emptyList(),
|
||||
override val state: LocalStateItem<State>,
|
||||
) : AddStateItem, HasOptions<Field>, HasState<Field.State> {
|
||||
override fun withOptions(options: List<FlatItemAction>): Field =
|
||||
copy(
|
||||
options = options,
|
||||
)
|
||||
|
||||
sealed interface State : HasOptions<State> {
|
||||
data class Text(
|
||||
override val options: List<FlatItemAction> = emptyList(),
|
||||
val label: TextFieldModel2,
|
||||
val text: TextFieldModel2,
|
||||
val hidden: Boolean = false,
|
||||
) : State {
|
||||
override fun withOptions(options: List<FlatItemAction>): Text =
|
||||
copy(
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
|
||||
data class Switch(
|
||||
override val options: List<FlatItemAction> = emptyList(),
|
||||
val checked: Boolean = false,
|
||||
val onCheckedChange: ((Boolean) -> Unit)? = null,
|
||||
val label: TextFieldModel2,
|
||||
) : State {
|
||||
override fun withOptions(options: List<FlatItemAction>): Switch =
|
||||
copy(
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
|
||||
data class LinkedId(
|
||||
override val options: List<FlatItemAction> = emptyList(),
|
||||
val value: DSecret.Field.LinkedId?,
|
||||
val actions: List<FlatItemAction>,
|
||||
val label: TextFieldModel2,
|
||||
) : State {
|
||||
override fun withOptions(options: List<FlatItemAction>): LinkedId =
|
||||
copy(
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Note(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<TextFieldModel2>,
|
||||
val markdown: Boolean,
|
||||
) : AddStateItem, HasState<TextFieldModel2>
|
||||
|
||||
data class Enum(
|
||||
override val id: String,
|
||||
val icon: ImageVector,
|
||||
val label: String,
|
||||
override val state: LocalStateItem<State>,
|
||||
) : AddStateItem, HasState<Enum.State> {
|
||||
data class State(
|
||||
val value: String = "",
|
||||
val dropdown: List<FlatItemAction> = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
data class Switch(
|
||||
override val id: String,
|
||||
val title: String,
|
||||
val text: String? = null,
|
||||
override val state: LocalStateItem<SwitchFieldModel>,
|
||||
) : AddStateItem, HasState<SwitchFieldModel>
|
||||
|
||||
data class Section(
|
||||
override val id: String,
|
||||
val text: String? = null,
|
||||
) : AddStateItem
|
||||
|
||||
//
|
||||
// CUSTOM
|
||||
//
|
||||
|
||||
data class DateMonthYear(
|
||||
override val id: String,
|
||||
override val state: LocalStateItem<State>,
|
||||
val label: String,
|
||||
) : AddStateItem, HasState<DateMonthYear.State> {
|
||||
data class State(
|
||||
val month: TextFieldModel2,
|
||||
val year: TextFieldModel2,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
||||
//
|
||||
// Items that may modify the amount of items in the
|
||||
// list.
|
||||
//
|
||||
|
||||
data class Add(
|
||||
override val id: String,
|
||||
val text: String,
|
||||
val actions: List<FlatItemAction>,
|
||||
) : AddStateItem
|
||||
}
|
||||
|
||||
data class LocalStateItem<T>(
|
||||
val flow: StateFlow<T>,
|
||||
val populator: CreateRequest.(T) -> CreateRequest = { this },
|
||||
)
|
||||
|
@ -95,6 +95,9 @@ import com.artemchep.keyguard.common.util.flow.combineToList
|
||||
import com.artemchep.keyguard.common.util.flow.foldAsList
|
||||
import com.artemchep.keyguard.common.util.flow.persistingStateIn
|
||||
import com.artemchep.keyguard.common.util.validLuhn
|
||||
import com.artemchep.keyguard.feature.add.AddStateItem
|
||||
import com.artemchep.keyguard.feature.add.AddStateOwnership
|
||||
import com.artemchep.keyguard.feature.add.LocalStateItem
|
||||
import com.artemchep.keyguard.feature.apppicker.AppPickerResult
|
||||
import com.artemchep.keyguard.feature.apppicker.AppPickerRoute
|
||||
import com.artemchep.keyguard.feature.auth.common.SwitchFieldModel
|
||||
@ -130,6 +133,7 @@ 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.buildContextItems
|
||||
import com.artemchep.keyguard.ui.icons.ChevronIcon
|
||||
import com.artemchep.keyguard.ui.icons.IconBox
|
||||
import com.artemchep.keyguard.ui.icons.Stub
|
||||
@ -458,7 +462,7 @@ fun produceAddScreenState(
|
||||
val item = AddStateItem.Add(
|
||||
id = "attachment.add",
|
||||
text = translate(Res.strings.list_add),
|
||||
actions = listOf(action),
|
||||
actions = persistentListOf(action),
|
||||
)
|
||||
flowOf(emptyList())
|
||||
},
|
||||
@ -522,7 +526,7 @@ fun produceAddScreenState(
|
||||
id = "reprompt",
|
||||
title = translate(Res.strings.additem_auth_reprompt_title),
|
||||
text = translate(Res.strings.additem_auth_reprompt_text),
|
||||
state = LocalStateItem(
|
||||
state = LocalStateItem<SwitchFieldModel, CreateRequest>(
|
||||
flow = kotlin.run {
|
||||
val sink = mutablePersistedFlow("reprompt") {
|
||||
args.initialValue?.reprompt
|
||||
@ -650,7 +654,7 @@ fun produceAddScreenState(
|
||||
},
|
||||
)
|
||||
|
||||
val titleItem = AddStateItem.Title(
|
||||
val titleItem = AddStateItem.Title<CreateRequest>(
|
||||
id = "title",
|
||||
state = LocalStateItem(
|
||||
flow = kotlin.run {
|
||||
@ -707,7 +711,7 @@ fun produceAddScreenState(
|
||||
.map { items ->
|
||||
items
|
||||
.mapNotNull { item ->
|
||||
val stateHolder = item as? AddStateItem.HasState<Any?>
|
||||
val stateHolder = item as? AddStateItem.HasState<Any?, CreateRequest>
|
||||
?: return@mapNotNull null
|
||||
|
||||
val state = stateHolder.state
|
||||
@ -858,7 +862,7 @@ fun produceAddScreenState(
|
||||
f
|
||||
}
|
||||
|
||||
class AddStateItemAttachmentFactory : Foo2Factory<AddStateItem.Attachment, DSecret.Attachment> {
|
||||
class AddStateItemAttachmentFactory : Foo2Factory<AddStateItem.Attachment<*>, DSecret.Attachment> {
|
||||
override val type: String = "attachment"
|
||||
|
||||
override fun RememberStateFlowScope.release(key: String) {
|
||||
@ -868,7 +872,7 @@ class AddStateItemAttachmentFactory : Foo2Factory<AddStateItem.Attachment, DSecr
|
||||
override fun RememberStateFlowScope.add(
|
||||
key: String,
|
||||
initial: DSecret.Attachment?,
|
||||
): AddStateItem.Attachment {
|
||||
): AddStateItem.Attachment<CreateRequest> {
|
||||
val nameKey = "$key.name"
|
||||
val nameSink = mutablePersistedFlow(nameKey) {
|
||||
initial?.fileName().orEmpty()
|
||||
@ -902,7 +906,7 @@ class AddStateItemAttachmentFactory : Foo2Factory<AddStateItem.Attachment, DSecr
|
||||
synced = initial != null,
|
||||
),
|
||||
)
|
||||
return AddStateItem.Attachment(
|
||||
return AddStateItem.Attachment<CreateRequest>(
|
||||
id = key,
|
||||
state = LocalStateItem(
|
||||
flow = stateFlow,
|
||||
@ -916,7 +920,7 @@ class AddStateItemAttachmentFactory : Foo2Factory<AddStateItem.Attachment, DSecr
|
||||
|
||||
class AddStateItemUriFactory(
|
||||
private val cipherUnsecureUrlCheck: CipherUnsecureUrlCheck,
|
||||
) : Foo2Factory<AddStateItem.Url, DSecret.Uri> {
|
||||
) : Foo2Factory<AddStateItem.Url<*>, DSecret.Uri> {
|
||||
override val type: String = "uri"
|
||||
|
||||
override fun RememberStateFlowScope.release(key: String) {
|
||||
@ -927,7 +931,7 @@ class AddStateItemUriFactory(
|
||||
override fun RememberStateFlowScope.add(
|
||||
key: String,
|
||||
initial: DSecret.Uri?,
|
||||
): AddStateItem.Url {
|
||||
): AddStateItem.Url<CreateRequest> {
|
||||
val uriKey = "$key.uri"
|
||||
val uriSink = mutablePersistedFlow(uriKey) {
|
||||
initial?.uri.orEmpty()
|
||||
@ -1018,7 +1022,10 @@ class AddStateItemUriFactory(
|
||||
}
|
||||
val actionsFlow = actionsMatchTypeItemFlow
|
||||
.map {
|
||||
listOf(it, actionsAppPickerItem)
|
||||
buildContextItems {
|
||||
this += it
|
||||
this += actionsAppPickerItem
|
||||
}
|
||||
}
|
||||
|
||||
val textFlow = uriSink
|
||||
@ -1090,7 +1097,7 @@ class AddStateItemUriFactory(
|
||||
matchType = DSecret.Uri.MatchType.default,
|
||||
),
|
||||
)
|
||||
return AddStateItem.Url(
|
||||
return AddStateItem.Url<CreateRequest>(
|
||||
id = key,
|
||||
state = LocalStateItem(
|
||||
flow = stateFlow,
|
||||
@ -1108,7 +1115,7 @@ class AddStateItemUriFactory(
|
||||
}
|
||||
|
||||
class AddStateItemPasskeyFactory(
|
||||
) : Foo2Factory<AddStateItem.Passkey, DSecret.Login.Fido2Credentials> {
|
||||
) : Foo2Factory<AddStateItem.Passkey<*>, DSecret.Login.Fido2Credentials> {
|
||||
@kotlinx.serialization.Serializable
|
||||
private data class PasskeyHolder(
|
||||
val data: PasskeyData? = null,
|
||||
@ -1158,7 +1165,7 @@ class AddStateItemPasskeyFactory(
|
||||
override fun RememberStateFlowScope.add(
|
||||
key: String,
|
||||
initial: DSecret.Login.Fido2Credentials?,
|
||||
): AddStateItem.Passkey {
|
||||
): AddStateItem.Passkey<CreateRequest> {
|
||||
val dataKey = "$key.data"
|
||||
val dataSink = mutablePersistedFlow(
|
||||
dataKey,
|
||||
@ -1203,7 +1210,7 @@ class AddStateItemPasskeyFactory(
|
||||
passkey = null,
|
||||
),
|
||||
)
|
||||
return AddStateItem.Passkey(
|
||||
return AddStateItem.Passkey<CreateRequest>(
|
||||
id = key,
|
||||
state = LocalStateItem(
|
||||
flow = stateFlow,
|
||||
@ -1220,11 +1227,11 @@ class AddStateItemPasskeyFactory(
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AddStateItemFieldFactory : Foo2Factory<AddStateItem.Field, DSecret.Field> {
|
||||
abstract class AddStateItemFieldFactory : Foo2Factory<AddStateItem.Field<*>, DSecret.Field> {
|
||||
fun foo(
|
||||
key: String,
|
||||
flow: StateFlow<AddStateItem.Field.State>,
|
||||
) = AddStateItem.Field(
|
||||
) = AddStateItem.Field<CreateRequest>(
|
||||
id = key,
|
||||
state = LocalStateItem(
|
||||
flow = flow,
|
||||
@ -1282,7 +1289,7 @@ class AddStateItemFieldTextFactory : AddStateItemFieldFactory() {
|
||||
override fun RememberStateFlowScope.add(
|
||||
key: String,
|
||||
initial: DSecret.Field?,
|
||||
): AddStateItem.Field {
|
||||
): AddStateItem.Field<CreateRequest> {
|
||||
val labelSink = mutablePersistedFlow("$key.label") {
|
||||
initial?.name.orEmpty()
|
||||
}
|
||||
@ -1342,9 +1349,9 @@ class AddStateItemFieldTextFactory : AddStateItemFieldFactory() {
|
||||
}
|
||||
val actionsFlow = actionsConcealItemFlow
|
||||
.map { concealItem ->
|
||||
listOf(
|
||||
concealItem,
|
||||
)
|
||||
buildContextItems {
|
||||
this += concealItem
|
||||
}
|
||||
}
|
||||
|
||||
val stateFlow = combine(
|
||||
@ -1386,7 +1393,7 @@ class AddStateItemFieldBooleanFactory : AddStateItemFieldFactory() {
|
||||
override fun RememberStateFlowScope.add(
|
||||
key: String,
|
||||
initial: DSecret.Field?,
|
||||
): AddStateItem.Field {
|
||||
): AddStateItem.Field<CreateRequest> {
|
||||
val labelSink = mutablePersistedFlow("$key.label") {
|
||||
initial?.name.orEmpty()
|
||||
}
|
||||
@ -1441,7 +1448,7 @@ class AddStateItemFieldLinkedIdFactory(
|
||||
override fun RememberStateFlowScope.add(
|
||||
key: String,
|
||||
initial: DSecret.Field?,
|
||||
): AddStateItem.Field {
|
||||
): AddStateItem.Field<CreateRequest> {
|
||||
val labelSink = mutablePersistedFlow("$key.label") {
|
||||
initial?.name.orEmpty()
|
||||
}
|
||||
@ -1488,11 +1495,13 @@ class AddStateItemFieldLinkedIdFactory(
|
||||
}
|
||||
val actionsFlow = typeFlow
|
||||
.map { type ->
|
||||
actionsAll
|
||||
.mapNotNull { (itemLinkedId, itemAction) ->
|
||||
itemAction
|
||||
.takeIf { itemLinkedId.type == type }
|
||||
}
|
||||
buildContextItems {
|
||||
actionsAll
|
||||
.forEach { (itemLinkedId, itemAction) ->
|
||||
this += itemAction
|
||||
.takeIf { itemLinkedId.type == type }
|
||||
}
|
||||
}
|
||||
}
|
||||
val stateFlow = combine(
|
||||
labelFlow,
|
||||
@ -1511,7 +1520,7 @@ class AddStateItemFieldLinkedIdFactory(
|
||||
initialValue = AddStateItem.Field.State.LinkedId(
|
||||
label = TextFieldModel2.empty,
|
||||
value = null,
|
||||
actions = emptyList(),
|
||||
actions = persistentListOf(),
|
||||
),
|
||||
)
|
||||
return foo(
|
||||
@ -1646,15 +1655,16 @@ private fun <Argument> FieldBakeryScope<Argument>.typeBasedAddItem(
|
||||
return@map null // hide add item
|
||||
}
|
||||
|
||||
val actions = types
|
||||
.map { type ->
|
||||
FlatItemAction(
|
||||
val actions = buildContextItems {
|
||||
types.forEach { type ->
|
||||
this += FlatItemAction(
|
||||
title = type.name,
|
||||
onClick = ::add
|
||||
.partially1(type.type)
|
||||
.partially1(null),
|
||||
)
|
||||
}
|
||||
}
|
||||
AddStateItem.Add(
|
||||
id = "$scope.add",
|
||||
text = translator.translate(Res.strings.list_add),
|
||||
@ -1795,35 +1805,35 @@ fun <T, Argument> RememberStateFlowScope.foo(
|
||||
|
||||
// Appends list-specific options to be able to reorder
|
||||
// the list or remove an item.
|
||||
val options = item.options.toMutableList()
|
||||
if (index > 0) {
|
||||
options += FlatItemAction(
|
||||
icon = Icons.Outlined.ArrowUpward,
|
||||
title = translate(Res.strings.list_move_up),
|
||||
onClick = ::moveUp.partially1(entry.key),
|
||||
val options = buildContextItems(item.options) {
|
||||
if (index > 0) {
|
||||
this += FlatItemAction(
|
||||
icon = Icons.Outlined.ArrowUpward,
|
||||
title = translate(Res.strings.list_move_up),
|
||||
onClick = ::moveUp.partially1(entry.key),
|
||||
)
|
||||
}
|
||||
if (index < state.size - 1) {
|
||||
this += FlatItemAction(
|
||||
icon = Icons.Outlined.ArrowDownward,
|
||||
title = translate(Res.strings.list_move_down),
|
||||
onClick = ::moveDown.partially1(entry.key),
|
||||
)
|
||||
}
|
||||
this += FlatItemAction(
|
||||
icon = Icons.Outlined.DeleteForever,
|
||||
title = translate(Res.strings.list_remove),
|
||||
onClick = {
|
||||
val intent = createConfirmationDialogIntent(
|
||||
icon = icon(Icons.Outlined.DeleteForever),
|
||||
title = translate(Res.strings.list_remove_confirmation_title),
|
||||
) {
|
||||
delete(entry.key)
|
||||
}
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
}
|
||||
if (index < state.size - 1) {
|
||||
options += FlatItemAction(
|
||||
icon = Icons.Outlined.ArrowDownward,
|
||||
title = translate(Res.strings.list_move_down),
|
||||
onClick = ::moveDown.partially1(entry.key),
|
||||
)
|
||||
}
|
||||
options += FlatItemAction(
|
||||
icon = Icons.Outlined.DeleteForever,
|
||||
title = translate(Res.strings.list_remove),
|
||||
onClick = {
|
||||
val intent = createConfirmationDialogIntent(
|
||||
icon = icon(Icons.Outlined.DeleteForever),
|
||||
title = translate(Res.strings.list_remove_confirmation_title),
|
||||
) {
|
||||
delete(entry.key)
|
||||
}
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
|
||||
item.withOptions(options)
|
||||
}
|
||||
}
|
||||
@ -1853,7 +1863,7 @@ private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
|
||||
data class Fool<T>(
|
||||
val value: T,
|
||||
val element: AddState.SaveToElement?,
|
||||
val element: AddStateOwnership.Element?,
|
||||
)
|
||||
|
||||
val disk = loadDiskHandle("new_item")
|
||||
@ -1882,31 +1892,31 @@ private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
// Make an account that has the most ciphers a
|
||||
// default account.
|
||||
val accountId = ioEffect {
|
||||
val accountIds = getProfiles().toIO().bind()
|
||||
.map { it.accountId() }
|
||||
.toSet()
|
||||
val accountIds = getProfiles().toIO().bind()
|
||||
.map { it.accountId() }
|
||||
.toSet()
|
||||
|
||||
fun String.takeIfAccountIdExists() = this
|
||||
.takeIf { id ->
|
||||
id in accountIds
|
||||
}
|
||||
|
||||
val lastAccountId = accountIdSink.value?.takeIfAccountIdExists()
|
||||
if (lastAccountId != null) {
|
||||
return@ioEffect lastAccountId
|
||||
fun String.takeIfAccountIdExists() = this
|
||||
.takeIf { id ->
|
||||
id in accountIds
|
||||
}
|
||||
|
||||
val ciphers = getCiphers().toIO().bind()
|
||||
ciphers
|
||||
.asSequence()
|
||||
.filter { it.organizationId != null }
|
||||
.groupBy { it.accountId }
|
||||
// the one that has the most ciphers
|
||||
.maxByOrNull { entry -> entry.value.size }
|
||||
// account id
|
||||
?.key
|
||||
?.takeIfAccountIdExists()
|
||||
}.attempt().bind().getOrNull()
|
||||
val lastAccountId = accountIdSink.value?.takeIfAccountIdExists()
|
||||
if (lastAccountId != null) {
|
||||
return@ioEffect lastAccountId
|
||||
}
|
||||
|
||||
val ciphers = getCiphers().toIO().bind()
|
||||
ciphers
|
||||
.asSequence()
|
||||
.filter { it.organizationId != null }
|
||||
.groupBy { it.accountId }
|
||||
// the one that has the most ciphers
|
||||
.maxByOrNull { entry -> entry.value.size }
|
||||
// account id
|
||||
?.key
|
||||
?.takeIfAccountIdExists()
|
||||
}.attempt().bind().getOrNull()
|
||||
|
||||
AddItemOwnershipData(
|
||||
accountId = accountId,
|
||||
@ -1934,11 +1944,12 @@ private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
getProfiles(),
|
||||
) { accountId, profiles ->
|
||||
if (accountId == null) {
|
||||
val item = AddState.SaveToElement.Item(
|
||||
val item = AddStateOwnership.Element.Item(
|
||||
key = "account.empty",
|
||||
title = translate(Res.strings.account_none),
|
||||
stub = true,
|
||||
)
|
||||
val el = AddState.SaveToElement(
|
||||
val el = AddStateOwnership.Element(
|
||||
readOnly = ro,
|
||||
items = listOf(item),
|
||||
)
|
||||
@ -1949,12 +1960,12 @@ private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
}
|
||||
val profileOrNull = profiles
|
||||
.firstOrNull { it.accountId() == accountId }
|
||||
val el = AddState.SaveToElement(
|
||||
val el = AddStateOwnership.Element(
|
||||
readOnly = ro,
|
||||
items = listOfNotNull(profileOrNull)
|
||||
.map { account ->
|
||||
val key = "account.${account.accountId()}"
|
||||
AddState.SaveToElement.Item(
|
||||
AddStateOwnership.Element.Item(
|
||||
key = key,
|
||||
title = account.email,
|
||||
text = account.accountHost,
|
||||
@ -1975,11 +1986,12 @@ private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
getOrganizations(),
|
||||
) { organizationId, organizations ->
|
||||
if (organizationId == null) {
|
||||
val item = AddState.SaveToElement.Item(
|
||||
val item = AddStateOwnership.Element.Item(
|
||||
key = "organization.empty",
|
||||
title = translate(Res.strings.organization_none),
|
||||
stub = true,
|
||||
)
|
||||
val el = AddState.SaveToElement(
|
||||
val el = AddStateOwnership.Element(
|
||||
readOnly = ro,
|
||||
items = listOf(item),
|
||||
)
|
||||
@ -1990,12 +2002,12 @@ private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
}
|
||||
val organizationOrNull = organizations
|
||||
.firstOrNull { it.id == organizationId }
|
||||
val el = AddState.SaveToElement(
|
||||
val el = AddStateOwnership.Element(
|
||||
readOnly = ro,
|
||||
items = listOfNotNull(organizationOrNull)
|
||||
.map { organization ->
|
||||
val key = "organization.${organization.id}"
|
||||
AddState.SaveToElement.Item(
|
||||
AddStateOwnership.Element.Item(
|
||||
key = key,
|
||||
title = organization.name,
|
||||
)
|
||||
@ -2023,12 +2035,12 @@ private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
val selectedCollections = collections
|
||||
.sortedWith(StringComparatorIgnoreCase { it.name })
|
||||
.filter { it.id in collectionIds }
|
||||
val el = AddState.SaveToElement(
|
||||
val el = AddStateOwnership.Element(
|
||||
readOnly = ro,
|
||||
items = selectedCollections
|
||||
.map { collection ->
|
||||
val key = "collection.${collection.id}"
|
||||
AddState.SaveToElement.Item(
|
||||
AddStateOwnership.Element.Item(
|
||||
key = key,
|
||||
title = collection.name,
|
||||
)
|
||||
@ -2048,11 +2060,12 @@ private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
) { selectedFolder, folders ->
|
||||
when (selectedFolder) {
|
||||
is FolderInfo.None -> {
|
||||
val item = AddState.SaveToElement.Item(
|
||||
val item = AddStateOwnership.Element.Item(
|
||||
key = "folder.empty",
|
||||
title = translate(Res.strings.folder_none),
|
||||
stub = true,
|
||||
)
|
||||
val el = AddState.SaveToElement(
|
||||
val el = AddStateOwnership.Element(
|
||||
readOnly = false,
|
||||
items = listOf(item),
|
||||
)
|
||||
@ -2063,11 +2076,12 @@ private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
}
|
||||
|
||||
is FolderInfo.New -> {
|
||||
val item = AddState.SaveToElement.Item(
|
||||
val item = AddStateOwnership.Element.Item(
|
||||
key = "folder.new",
|
||||
title = selectedFolder.name,
|
||||
stub = true,
|
||||
)
|
||||
val el = AddState.SaveToElement(
|
||||
val el = AddStateOwnership.Element(
|
||||
readOnly = false,
|
||||
items = listOf(item),
|
||||
)
|
||||
@ -2080,12 +2094,12 @@ private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
is FolderInfo.Id -> {
|
||||
val selectedFolderOrNull = folders
|
||||
.firstOrNull { it.id == selectedFolder.id }
|
||||
val el = AddState.SaveToElement(
|
||||
val el = AddStateOwnership.Element(
|
||||
readOnly = false,
|
||||
items = listOfNotNull(selectedFolderOrNull)
|
||||
.map { folder ->
|
||||
val key = "folder.${folder.id}"
|
||||
AddState.SaveToElement.Item(
|
||||
AddStateOwnership.Element.Item(
|
||||
key = key,
|
||||
title = folder.name,
|
||||
)
|
||||
@ -2118,8 +2132,7 @@ private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
organizationId = organization.value,
|
||||
collectionIds = collection.value,
|
||||
)
|
||||
AddState.Ownership(
|
||||
data = data,
|
||||
val ui = AddStateOwnership(
|
||||
account = account.element,
|
||||
organization = organization.element.takeIf { account.value != null },
|
||||
collection = collection.element.takeIf { account.value != null },
|
||||
@ -2129,7 +2142,7 @@ private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
route = OrganizationConfirmationRoute(
|
||||
args = OrganizationConfirmationRoute.Args(
|
||||
decor = OrganizationConfirmationRoute.Args.Decor(
|
||||
title = "Save to",
|
||||
title = translate(Res.strings.save_to),
|
||||
icon = Icons.Outlined.AccountBox,
|
||||
),
|
||||
flags = flags,
|
||||
@ -2154,50 +2167,54 @@ private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
AddState.Ownership(
|
||||
data = data,
|
||||
ui = ui,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class TmpLogin(
|
||||
val username: AddStateItem.Username,
|
||||
val password: AddStateItem.Password,
|
||||
val totp: AddStateItem.Totp,
|
||||
val username: AddStateItem.Username<CreateRequest>,
|
||||
val password: AddStateItem.Password<CreateRequest>,
|
||||
val totp: AddStateItem.Totp<CreateRequest>,
|
||||
val items: List<AddStateItem>,
|
||||
)
|
||||
|
||||
data class TmpCard(
|
||||
val cardholderName: AddStateItem.Text,
|
||||
val brand: AddStateItem.Text,
|
||||
val number: AddStateItem.Text,
|
||||
val fromDate: AddStateItem.DateMonthYear,
|
||||
val expDate: AddStateItem.DateMonthYear,
|
||||
val code: AddStateItem.Text,
|
||||
val cardholderName: AddStateItem.Text<CreateRequest>,
|
||||
val brand: AddStateItem.Text<CreateRequest>,
|
||||
val number: AddStateItem.Text<CreateRequest>,
|
||||
val fromDate: AddStateItem.DateMonthYear<CreateRequest>,
|
||||
val expDate: AddStateItem.DateMonthYear<CreateRequest>,
|
||||
val code: AddStateItem.Text<CreateRequest>,
|
||||
val items: List<AddStateItem>,
|
||||
)
|
||||
|
||||
data class TmpIdentity(
|
||||
val title: AddStateItem.Text,
|
||||
val firstName: AddStateItem.Text,
|
||||
val middleName: AddStateItem.Text,
|
||||
val lastName: AddStateItem.Text,
|
||||
val address1: AddStateItem.Text,
|
||||
val address2: AddStateItem.Text,
|
||||
val address3: AddStateItem.Text,
|
||||
val city: AddStateItem.Text,
|
||||
val state: AddStateItem.Text,
|
||||
val postalCode: AddStateItem.Text,
|
||||
val country: AddStateItem.Text,
|
||||
val company: AddStateItem.Text,
|
||||
val email: AddStateItem.Text,
|
||||
val phone: AddStateItem.Text,
|
||||
val ssn: AddStateItem.Text,
|
||||
val username: AddStateItem.Text,
|
||||
val passportNumber: AddStateItem.Text,
|
||||
val licenseNumber: AddStateItem.Text,
|
||||
val title: AddStateItem.Text<CreateRequest>,
|
||||
val firstName: AddStateItem.Text<CreateRequest>,
|
||||
val middleName: AddStateItem.Text<CreateRequest>,
|
||||
val lastName: AddStateItem.Text<CreateRequest>,
|
||||
val address1: AddStateItem.Text<CreateRequest>,
|
||||
val address2: AddStateItem.Text<CreateRequest>,
|
||||
val address3: AddStateItem.Text<CreateRequest>,
|
||||
val city: AddStateItem.Text<CreateRequest>,
|
||||
val state: AddStateItem.Text<CreateRequest>,
|
||||
val postalCode: AddStateItem.Text<CreateRequest>,
|
||||
val country: AddStateItem.Text<CreateRequest>,
|
||||
val company: AddStateItem.Text<CreateRequest>,
|
||||
val email: AddStateItem.Text<CreateRequest>,
|
||||
val phone: AddStateItem.Text<CreateRequest>,
|
||||
val ssn: AddStateItem.Text<CreateRequest>,
|
||||
val username: AddStateItem.Text<CreateRequest>,
|
||||
val passportNumber: AddStateItem.Text<CreateRequest>,
|
||||
val licenseNumber: AddStateItem.Text<CreateRequest>,
|
||||
val items: List<AddStateItem>,
|
||||
)
|
||||
|
||||
data class TmpNote(
|
||||
val note: AddStateItem.Note,
|
||||
val note: AddStateItem.Note<CreateRequest>,
|
||||
val items: List<AddStateItem>,
|
||||
)
|
||||
|
||||
@ -2214,7 +2231,7 @@ private suspend fun RememberStateFlowScope.produceLoginState(
|
||||
key: String,
|
||||
initialValue: String? = null,
|
||||
populator: CreateRequest.(AddStateItem.Username.State) -> CreateRequest,
|
||||
factory: (String, LocalStateItem<AddStateItem.Username.State>) -> Item,
|
||||
factory: (String, LocalStateItem<AddStateItem.Username.State, CreateRequest>) -> Item,
|
||||
) = kotlin.run {
|
||||
val id = "$prefix.$key"
|
||||
|
||||
@ -2225,9 +2242,10 @@ private suspend fun RememberStateFlowScope.produceLoginState(
|
||||
LocalStateItem(
|
||||
flow = ownershipFlow
|
||||
.map {
|
||||
it.account
|
||||
it.ui.account
|
||||
?.items
|
||||
?.asSequence()
|
||||
?.filter { !it.stub }
|
||||
?.map { it.title }
|
||||
?.toPersistentList()
|
||||
?: persistentListOf()
|
||||
@ -2262,7 +2280,7 @@ private suspend fun RememberStateFlowScope.produceLoginState(
|
||||
key: String,
|
||||
initialValue: String? = null,
|
||||
populator: CreateRequest.(TextFieldModel2) -> CreateRequest,
|
||||
factory: (String, LocalStateItem<TextFieldModel2>) -> Item,
|
||||
factory: (String, LocalStateItem<TextFieldModel2, CreateRequest>) -> Item,
|
||||
) = kotlin.run {
|
||||
val id = "$prefix.$key"
|
||||
|
||||
@ -2381,7 +2399,7 @@ private suspend fun RememberStateFlowScope.produceLoginState(
|
||||
totpToken = totp,
|
||||
)
|
||||
}
|
||||
LocalStateItem(
|
||||
LocalStateItem<AddStateItem.Totp.State, CreateRequest>(
|
||||
flow = flow
|
||||
.persistingStateIn(
|
||||
scope = screenScope,
|
||||
@ -2444,7 +2462,7 @@ private suspend fun RememberStateFlowScope.produceCardState(
|
||||
initialMonth: String? = null,
|
||||
initialYear: String? = null,
|
||||
populator: CreateRequest.(AddStateItem.DateMonthYear.State) -> CreateRequest,
|
||||
factory: (String, LocalStateItem<AddStateItem.DateMonthYear.State>) -> Item,
|
||||
factory: (String, LocalStateItem<AddStateItem.DateMonthYear.State, CreateRequest>) -> Item,
|
||||
): Item {
|
||||
val id = "$prefix.$key"
|
||||
|
||||
@ -2557,7 +2575,7 @@ private suspend fun RememberStateFlowScope.produceCardState(
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
lens: Optional<CreateRequest, String>,
|
||||
) = createItem(
|
||||
) = createItem<CreateRequest>(
|
||||
prefix = prefix,
|
||||
key = key,
|
||||
label = label,
|
||||
@ -2587,7 +2605,7 @@ private suspend fun RememberStateFlowScope.produceCardState(
|
||||
|
||||
val sink = mutablePersistedFlow(id) { initialValue.orEmpty() }
|
||||
val state = mutableComposeState(sink)
|
||||
val state2 = LocalStateItem(
|
||||
val state2 = LocalStateItem<AddStateItem.Text.State, CreateRequest>(
|
||||
flow = sink
|
||||
.map { value ->
|
||||
val isValid = kotlin.run {
|
||||
@ -2899,7 +2917,7 @@ private suspend fun RememberStateFlowScope.produceIdentityState(
|
||||
autocompleteOptions: ImmutableList<String> = persistentListOf(),
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
lens: Optional<CreateRequest, String>,
|
||||
) = createItem(
|
||||
) = createItem<CreateRequest>(
|
||||
prefix = prefix,
|
||||
key = key,
|
||||
label = label,
|
||||
@ -3255,7 +3273,7 @@ private suspend fun RememberStateFlowScope.produceNoteState(
|
||||
|
||||
val sink = mutablePersistedFlow(id) { initialValue.orEmpty() }
|
||||
val state = mutableComposeState(sink)
|
||||
val stateItem = LocalStateItem(
|
||||
val stateItem = LocalStateItem<TextFieldModel2, CreateRequest>(
|
||||
flow = sink
|
||||
.map { value ->
|
||||
val model = TextFieldModel2(
|
||||
@ -3300,7 +3318,7 @@ private suspend fun RememberStateFlowScope.produceNoteState(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun RememberStateFlowScope.createItem(
|
||||
suspend fun <Request> RememberStateFlowScope.createItem(
|
||||
prefix: String,
|
||||
key: String,
|
||||
label: String? = null,
|
||||
@ -3310,7 +3328,7 @@ private suspend fun RememberStateFlowScope.createItem(
|
||||
singleLine: Boolean = false,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
populator: CreateRequest.(AddStateItem.Text.State) -> CreateRequest,
|
||||
populator: Request.(AddStateItem.Text.State) -> Request,
|
||||
) = createItem(
|
||||
prefix = prefix,
|
||||
key = key,
|
||||
@ -3329,7 +3347,7 @@ private suspend fun RememberStateFlowScope.createItem(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <Item> RememberStateFlowScope.createItem(
|
||||
suspend fun <Item, Request> RememberStateFlowScope.createItem(
|
||||
prefix: String,
|
||||
key: String,
|
||||
label: String? = null,
|
||||
@ -3339,8 +3357,8 @@ private suspend fun <Item> RememberStateFlowScope.createItem(
|
||||
singleLine: Boolean = false,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
populator: CreateRequest.(AddStateItem.Text.State) -> CreateRequest,
|
||||
factory: (String, LocalStateItem<AddStateItem.Text.State>) -> Item,
|
||||
populator: Request.(AddStateItem.Text.State) -> Request,
|
||||
factory: (String, LocalStateItem<AddStateItem.Text.State, Request>) -> Item,
|
||||
): Item {
|
||||
val id = "$prefix.$key"
|
||||
|
||||
@ -3384,7 +3402,7 @@ private suspend fun RememberStateFlowScope.createItem2Txt(
|
||||
key: String,
|
||||
args: AddRoute.Args,
|
||||
getSuggestion: (DSecret) -> String?,
|
||||
field: AddStateItem.Text,
|
||||
field: AddStateItem.Text<CreateRequest>,
|
||||
concealed: Boolean = false,
|
||||
) = createItem2(
|
||||
prefix = prefix,
|
||||
@ -3406,7 +3424,7 @@ private suspend fun RememberStateFlowScope.createItem2(
|
||||
selectedFlow: Flow<String>,
|
||||
concealed: Boolean = false,
|
||||
onClick: (String) -> Unit,
|
||||
): AddStateItem.Suggestion? {
|
||||
): AddStateItem.Suggestion<CreateRequest>? {
|
||||
val finalKey = "$prefix.$key.suggestions"
|
||||
val suggestions = args.merge
|
||||
?.ciphers
|
||||
|
@ -1,17 +1,18 @@
|
||||
package com.artemchep.keyguard.feature.home.vault.add.attachment
|
||||
|
||||
import com.artemchep.keyguard.common.model.create.CreateRequest
|
||||
import com.artemchep.keyguard.common.model.create.CreateSendRequest
|
||||
import com.artemchep.keyguard.common.model.create.attachments
|
||||
import com.artemchep.keyguard.common.util.flow.persistingStateIn
|
||||
import com.artemchep.keyguard.feature.add.AddStateItem
|
||||
import com.artemchep.keyguard.feature.add.LocalStateItem
|
||||
import com.artemchep.keyguard.feature.auth.common.TextFieldModel2
|
||||
import com.artemchep.keyguard.feature.home.vault.add.AddStateItem
|
||||
import com.artemchep.keyguard.feature.home.vault.add.Foo2Factory
|
||||
import com.artemchep.keyguard.feature.home.vault.add.LocalStateItem
|
||||
import com.artemchep.keyguard.feature.navigation.state.RememberStateFlowScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class SkeletonAttachmentItemFactory : Foo2Factory<AddStateItem.Attachment, SkeletonAttachment> {
|
||||
class SkeletonAttachmentItemFactory : Foo2Factory<AddStateItem.Attachment<*>, SkeletonAttachment> {
|
||||
override val type: String = "attachment"
|
||||
|
||||
override fun RememberStateFlowScope.release(key: String) {
|
||||
@ -22,7 +23,7 @@ class SkeletonAttachmentItemFactory : Foo2Factory<AddStateItem.Attachment, Skele
|
||||
override fun RememberStateFlowScope.add(
|
||||
key: String,
|
||||
initial: SkeletonAttachment?,
|
||||
): AddStateItem.Attachment {
|
||||
): AddStateItem.Attachment<CreateRequest> {
|
||||
val identitySink = mutablePersistedFlow("$key.identity") {
|
||||
val identity = initial?.identity
|
||||
requireNotNull(identity)
|
||||
|
@ -681,7 +681,7 @@ fun RememberStateFlowScope.cipherDeleteAction(
|
||||
route = ConfirmationRoute(
|
||||
args = ConfirmationRoute.Args(
|
||||
icon = icon(Icons.Outlined.DeleteForever),
|
||||
title = "Delete forever?",
|
||||
title = translate(Res.strings.ciphers_action_delete_confirmation_title),
|
||||
),
|
||||
),
|
||||
) { result ->
|
||||
|
@ -44,7 +44,7 @@ suspend fun DSend.toVaultListItem(
|
||||
groupId = groupId,
|
||||
revisionDate = revisionDate,
|
||||
createdDate = createdDate,
|
||||
hasPassword = password != null,
|
||||
hasPassword = hasPassword,
|
||||
hasFile = type == DSend.Type.File,
|
||||
icon = icon,
|
||||
type = type.name,
|
||||
|
@ -7,8 +7,11 @@ import com.artemchep.keyguard.common.model.DSend
|
||||
import com.artemchep.keyguard.feature.auth.common.TextFieldModel2
|
||||
import com.artemchep.keyguard.feature.send.search.SendSortItem
|
||||
import com.artemchep.keyguard.feature.send.search.filter.SendFilterItem
|
||||
import com.artemchep.keyguard.ui.ContextItem
|
||||
import com.artemchep.keyguard.ui.FlatItemAction
|
||||
import com.artemchep.keyguard.ui.Selection
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
|
||||
@ -23,7 +26,7 @@ data class SendListState(
|
||||
val clearFilters: (() -> Unit)? = null,
|
||||
val clearSort: (() -> Unit)? = null,
|
||||
val showKeyboard: Boolean = false,
|
||||
val primaryActions: List<FlatItemAction> = emptyList(),
|
||||
val primaryActions: ImmutableList<ContextItem> = persistentListOf(),
|
||||
val actions: List<FlatItemAction> = emptyList(),
|
||||
val content: Content = Content.Skeleton,
|
||||
val sideEffects: SideEffects = SideEffects(),
|
||||
|
@ -34,6 +34,7 @@ import com.artemchep.keyguard.common.usecase.GetSends
|
||||
import com.artemchep.keyguard.common.usecase.GetSuggestions
|
||||
import com.artemchep.keyguard.common.usecase.GetWebsiteIcons
|
||||
import com.artemchep.keyguard.common.usecase.QueueSyncAll
|
||||
import com.artemchep.keyguard.common.usecase.SendToolbox
|
||||
import com.artemchep.keyguard.common.usecase.SupervisorRead
|
||||
import com.artemchep.keyguard.common.usecase.filterHiddenProfiles
|
||||
import com.artemchep.keyguard.common.util.flow.EventFlow
|
||||
@ -59,6 +60,7 @@ import com.artemchep.keyguard.feature.navigation.state.PersistedStorage
|
||||
import com.artemchep.keyguard.feature.navigation.state.copy
|
||||
import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
||||
import com.artemchep.keyguard.feature.search.search.SEARCH_DEBOUNCE
|
||||
import com.artemchep.keyguard.feature.send.add.SendAddRoute
|
||||
import com.artemchep.keyguard.feature.send.search.AccessCountSendSort
|
||||
import com.artemchep.keyguard.feature.send.search.AlphabeticalSendSort
|
||||
import com.artemchep.keyguard.feature.send.search.LastDeletedSendSort
|
||||
@ -70,23 +72,28 @@ import com.artemchep.keyguard.feature.send.search.SendSortItem
|
||||
import com.artemchep.keyguard.feature.send.search.ah
|
||||
import com.artemchep.keyguard.feature.send.search.createFilter
|
||||
import com.artemchep.keyguard.feature.send.search.filter.FilterSendHolder
|
||||
import com.artemchep.keyguard.feature.send.util.SendUtil
|
||||
import com.artemchep.keyguard.leof
|
||||
import com.artemchep.keyguard.platform.parcelize.LeParcelable
|
||||
import com.artemchep.keyguard.platform.parcelize.LeParcelize
|
||||
import com.artemchep.keyguard.platform.util.isRelease
|
||||
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.KeyguardView
|
||||
import com.artemchep.keyguard.ui.icons.SyncIcon
|
||||
import com.artemchep.keyguard.ui.icons.icon
|
||||
import com.artemchep.keyguard.ui.selection.selectionHandle
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@ -177,7 +184,7 @@ fun sendListScreenState(
|
||||
getProfiles: GetProfiles,
|
||||
getAppIcons: GetAppIcons,
|
||||
getWebsiteIcons: GetWebsiteIcons,
|
||||
toolbox: CipherToolbox,
|
||||
toolbox: SendToolbox,
|
||||
queueSyncAll: QueueSyncAll,
|
||||
syncSupervisor: SupervisorRead,
|
||||
dateFormatter: DateFormatter,
|
||||
@ -227,6 +234,11 @@ fun sendListScreenState(
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
val canEditFlow = SendUtil.canEditFlow(
|
||||
profilesFlow = getProfiles(),
|
||||
canWriteFlow = getCanWrite(),
|
||||
)
|
||||
|
||||
val showKeyboardSink = if (args.canAlwaysShowKeyboard) {
|
||||
mutablePersistedFlow(
|
||||
key = "keyboard",
|
||||
@ -677,7 +689,50 @@ fun sendListScreenState(
|
||||
)
|
||||
.stateIn(this, SharingStarted.WhileSubscribed(), OurFilterResult())
|
||||
|
||||
val selectionFlow = flowOf<Selection?>(null)
|
||||
fun createTypeAction(
|
||||
type: DSend.Type,
|
||||
) = FlatItemAction(
|
||||
leading = icon(type.iconImageVector()),
|
||||
title = translate(type.titleH()),
|
||||
onClick = {
|
||||
val route = SendAddRoute(
|
||||
args = SendAddRoute.Args(
|
||||
type = type,
|
||||
),
|
||||
)
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
|
||||
val primaryActionsAll = buildContextItems {
|
||||
this += createTypeAction(
|
||||
type = DSend.Type.Text,
|
||||
)
|
||||
this += createTypeAction(
|
||||
type = DSend.Type.File,
|
||||
)
|
||||
}
|
||||
val primaryActionsFlow = kotlin.run {
|
||||
combine(
|
||||
canEditFlow,
|
||||
selectionHandle.idsFlow,
|
||||
) { canEdit, selectedItemIds ->
|
||||
if (canEdit && selectedItemIds.isEmpty() && !isRelease) {
|
||||
primaryActionsAll
|
||||
} else {
|
||||
// No items
|
||||
persistentListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val selectionFlow = SendUtil.selectionFlow(
|
||||
selectionHandle = selectionHandle,
|
||||
sendsFlow = ciphersRawFlow,
|
||||
canEditFlow = canEditFlow,
|
||||
toolbox = toolbox,
|
||||
)
|
||||
|
||||
val itemsFlow = ciphersFilteredFlow
|
||||
.combine(selectionFlow) { ciphers, selection ->
|
||||
@ -792,49 +847,6 @@ fun sendListScreenState(
|
||||
)
|
||||
}
|
||||
|
||||
fun createTypeAction(
|
||||
type: DSecret.Type,
|
||||
) = FlatItemAction(
|
||||
leading = icon(type.iconImageVector()),
|
||||
title = translate(type.titleH()),
|
||||
onClick = {
|
||||
val autofill = when (mode) {
|
||||
is AppMode.Main -> null
|
||||
is AppMode.PickPasskey -> null
|
||||
is AppMode.SavePasskey -> null
|
||||
is AppMode.Save -> {
|
||||
AddRoute.Args.Autofill.leof(mode.args)
|
||||
}
|
||||
|
||||
is AppMode.Pick -> {
|
||||
AddRoute.Args.Autofill.leof(mode.args)
|
||||
}
|
||||
}
|
||||
val route = LeAddRoute(
|
||||
args = AddRoute.Args(
|
||||
type = type,
|
||||
autofill = autofill,
|
||||
),
|
||||
)
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
|
||||
val primaryActions = listOf(
|
||||
createTypeAction(
|
||||
type = DSecret.Type.Login,
|
||||
),
|
||||
createTypeAction(
|
||||
type = DSecret.Type.Card,
|
||||
),
|
||||
createTypeAction(
|
||||
type = DSecret.Type.Identity,
|
||||
),
|
||||
createTypeAction(
|
||||
type = DSecret.Type.SecureNote,
|
||||
),
|
||||
)
|
||||
SendListState(
|
||||
revision = revision,
|
||||
query = queryField,
|
||||
@ -842,7 +854,6 @@ fun sendListScreenState(
|
||||
sort = comparators
|
||||
.takeIf { queryTrimmed.isEmpty() }
|
||||
.orEmpty(),
|
||||
primaryActions = primaryActions,
|
||||
saveFilters = null,
|
||||
clearFilters = filters.onClear,
|
||||
clearSort = if (sortDefault != sort) {
|
||||
@ -855,16 +866,9 @@ fun sendListScreenState(
|
||||
content = content,
|
||||
sideEffects = SendListState.SideEffects(cipherSink),
|
||||
)
|
||||
}.combine(
|
||||
combine(
|
||||
getCanWrite(),
|
||||
selectionHandle.idsFlow,
|
||||
) { canWrite, itemIds ->
|
||||
canWrite && itemIds.isEmpty()
|
||||
},
|
||||
) { state, canWrite ->
|
||||
}.combine(primaryActionsFlow) { state, actions ->
|
||||
state.copy(
|
||||
primaryActions = state.primaryActions.takeIf { canWrite }.orEmpty(),
|
||||
primaryActions = actions,
|
||||
)
|
||||
}.combine(actionsFlow) { state, actions ->
|
||||
state.copy(
|
||||
|
@ -0,0 +1,24 @@
|
||||
package com.artemchep.keyguard.feature.send.add
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.artemchep.keyguard.common.model.DSend
|
||||
import com.artemchep.keyguard.feature.navigation.Route
|
||||
|
||||
class SendAddRoute(
|
||||
val args: Args,
|
||||
) : Route {
|
||||
data class Args(
|
||||
val type: DSend.Type,
|
||||
val name: String? = null,
|
||||
val text: String? = null,
|
||||
val initialValue: DSend? = null,
|
||||
val ownershipRo: Boolean = initialValue != null,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
SendAddScreen(
|
||||
args = args,
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
package com.artemchep.keyguard.feature.send.add
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Save
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.artemchep.keyguard.common.model.Loadable
|
||||
import com.artemchep.keyguard.common.model.fold
|
||||
import com.artemchep.keyguard.common.model.getOrNull
|
||||
import com.artemchep.keyguard.feature.add.AddScreenItems
|
||||
import com.artemchep.keyguard.feature.add.AddScreenScope
|
||||
import com.artemchep.keyguard.feature.add.ToolbarContent
|
||||
import com.artemchep.keyguard.feature.add.ToolbarContentItemErrSkeleton
|
||||
import com.artemchep.keyguard.feature.home.vault.add.AddState
|
||||
import com.artemchep.keyguard.feature.home.vault.component.Section
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIcon
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import com.artemchep.keyguard.ui.DefaultFab
|
||||
import com.artemchep.keyguard.ui.ExpandedIfNotEmpty
|
||||
import com.artemchep.keyguard.ui.FabState
|
||||
import com.artemchep.keyguard.ui.FlatItemLayout
|
||||
import com.artemchep.keyguard.ui.FlatSimpleNote
|
||||
import com.artemchep.keyguard.ui.OptionsButton
|
||||
import com.artemchep.keyguard.ui.ScaffoldColumn
|
||||
import com.artemchep.keyguard.ui.button.FavouriteToggleButton
|
||||
import com.artemchep.keyguard.ui.shimmer.shimmer
|
||||
import com.artemchep.keyguard.ui.skeleton.SkeletonText
|
||||
import com.artemchep.keyguard.ui.toolbar.LargeToolbar
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
fun SendAddScreen(
|
||||
args: SendAddRoute.Args,
|
||||
) {
|
||||
val loadableState = produceSendAddScreenState(
|
||||
args = args,
|
||||
)
|
||||
val addScreenScope = remember {
|
||||
AddScreenScope(
|
||||
initialFocusRequested = true,
|
||||
)
|
||||
}
|
||||
|
||||
SendAddScreen(
|
||||
addScreenScope = addScreenScope,
|
||||
loadableState = loadableState,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SendAddScreen(
|
||||
addScreenScope: AddScreenScope,
|
||||
loadableState: Loadable<SendAddState>,
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||
ScaffoldColumn(
|
||||
modifier = Modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topAppBarScrollBehavior = scrollBehavior,
|
||||
topBar = {
|
||||
LargeToolbar(
|
||||
title = {
|
||||
val title = loadableState.getOrNull()?.title
|
||||
if (title != null) {
|
||||
Text(title)
|
||||
} else {
|
||||
SkeletonText(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.4f),
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
NavigationIcon()
|
||||
},
|
||||
actions = {
|
||||
val actions = loadableState.getOrNull()?.actions.orEmpty()
|
||||
OptionsButton(
|
||||
actions = actions,
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
floatingActionState = run {
|
||||
val fabOnClick = loadableState.getOrNull()?.onSave
|
||||
val fabState = if (fabOnClick != null) {
|
||||
FabState(
|
||||
onClick = fabOnClick,
|
||||
model = null,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
rememberUpdatedState(newValue = fabState)
|
||||
},
|
||||
floatingActionButton = {
|
||||
DefaultFab(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Save,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(Res.strings.save),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
columnVerticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
populateItems(
|
||||
addScreenScope = addScreenScope,
|
||||
loadableState = loadableState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.populateItems(
|
||||
addScreenScope: AddScreenScope,
|
||||
loadableState: Loadable<SendAddState>,
|
||||
) = loadableState.fold(
|
||||
ifLoading = {
|
||||
populateItemsSkeleton(
|
||||
addScreenScope = addScreenScope,
|
||||
)
|
||||
},
|
||||
ifOk = { state ->
|
||||
populateItemsContent(
|
||||
addScreenScope = addScreenScope,
|
||||
state = state,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.populateItemsSkeleton(
|
||||
addScreenScope: AddScreenScope,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 8.dp,
|
||||
vertical = 2.dp,
|
||||
)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
|
||||
) {
|
||||
ToolbarContentItemErrSkeleton(
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp),
|
||||
fraction = 0.5f,
|
||||
)
|
||||
}
|
||||
Section()
|
||||
Spacer(Modifier.height(24.dp))
|
||||
with(addScreenScope) {
|
||||
AddScreenItems()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.populateItemsContent(
|
||||
addScreenScope: AddScreenScope,
|
||||
state: SendAddState,
|
||||
) {
|
||||
ToolbarContent(
|
||||
modifier = Modifier,
|
||||
account = state.ownership.ui.account,
|
||||
organization = state.ownership.ui.organization,
|
||||
collection = state.ownership.ui.collection,
|
||||
folder = state.ownership.ui.folder,
|
||||
onClick = state.ownership.ui.onClick,
|
||||
)
|
||||
Section()
|
||||
Spacer(Modifier.height(24.dp))
|
||||
with(addScreenScope) {
|
||||
AddScreenItems(
|
||||
items = state.items,
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.artemchep.keyguard.feature.send.add
|
||||
|
||||
import com.artemchep.keyguard.feature.add.AddStateItem
|
||||
import com.artemchep.keyguard.feature.add.AddStateOwnership
|
||||
import com.artemchep.keyguard.ui.FlatItemAction
|
||||
|
||||
data class SendAddState(
|
||||
val title: String = "",
|
||||
val ownership: Ownership,
|
||||
val actions: List<FlatItemAction> = emptyList(),
|
||||
val items: List<AddStateItem> = emptyList(),
|
||||
val onSave: (() -> Unit)? = null,
|
||||
) {
|
||||
data class Ownership(
|
||||
val data: Data,
|
||||
val ui: AddStateOwnership,
|
||||
) {
|
||||
data class Data(
|
||||
val accountId: String?,
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,723 @@
|
||||
package com.artemchep.keyguard.feature.send.add
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AccountBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import arrow.core.flatten
|
||||
import arrow.core.partially2
|
||||
import arrow.optics.Optional
|
||||
import com.artemchep.keyguard.common.io.attempt
|
||||
import com.artemchep.keyguard.common.io.bind
|
||||
import com.artemchep.keyguard.common.io.effectTap
|
||||
import com.artemchep.keyguard.common.io.ioEffect
|
||||
import com.artemchep.keyguard.common.io.launchIn
|
||||
import com.artemchep.keyguard.common.io.toIO
|
||||
import com.artemchep.keyguard.common.model.DSecret
|
||||
import com.artemchep.keyguard.common.model.DSend
|
||||
import com.artemchep.keyguard.common.model.Loadable
|
||||
import com.artemchep.keyguard.common.model.create.CreateRequest
|
||||
import com.artemchep.keyguard.common.model.create.CreateSendRequest
|
||||
import com.artemchep.keyguard.common.model.create.address1
|
||||
import com.artemchep.keyguard.common.model.create.address2
|
||||
import com.artemchep.keyguard.common.model.create.address3
|
||||
import com.artemchep.keyguard.common.model.create.city
|
||||
import com.artemchep.keyguard.common.model.create.company
|
||||
import com.artemchep.keyguard.common.model.create.country
|
||||
import com.artemchep.keyguard.common.model.create.email
|
||||
import com.artemchep.keyguard.common.model.create.firstName
|
||||
import com.artemchep.keyguard.common.model.create.identity
|
||||
import com.artemchep.keyguard.common.model.create.lastName
|
||||
import com.artemchep.keyguard.common.model.create.licenseNumber
|
||||
import com.artemchep.keyguard.common.model.create.middleName
|
||||
import com.artemchep.keyguard.common.model.create.note
|
||||
import com.artemchep.keyguard.common.model.create.passportNumber
|
||||
import com.artemchep.keyguard.common.model.create.phone
|
||||
import com.artemchep.keyguard.common.model.create.postalCode
|
||||
import com.artemchep.keyguard.common.model.create.ssn
|
||||
import com.artemchep.keyguard.common.model.create.state
|
||||
import com.artemchep.keyguard.common.model.create.text
|
||||
import com.artemchep.keyguard.common.model.create.title
|
||||
import com.artemchep.keyguard.common.model.create.username
|
||||
import com.artemchep.keyguard.common.model.titleH
|
||||
import com.artemchep.keyguard.common.service.clipboard.ClipboardService
|
||||
import com.artemchep.keyguard.common.service.logging.LogRepository
|
||||
import com.artemchep.keyguard.common.usecase.AddSend
|
||||
import com.artemchep.keyguard.common.usecase.CipherUnsecureUrlCheck
|
||||
import com.artemchep.keyguard.common.usecase.GetAccounts
|
||||
import com.artemchep.keyguard.common.usecase.GetCiphers
|
||||
import com.artemchep.keyguard.common.usecase.GetCollections
|
||||
import com.artemchep.keyguard.common.usecase.GetFolders
|
||||
import com.artemchep.keyguard.common.usecase.GetGravatarUrl
|
||||
import com.artemchep.keyguard.common.usecase.GetMarkdown
|
||||
import com.artemchep.keyguard.common.usecase.GetOrganizations
|
||||
import com.artemchep.keyguard.common.usecase.GetProfiles
|
||||
import com.artemchep.keyguard.common.usecase.GetSends
|
||||
import com.artemchep.keyguard.common.usecase.GetTotpCode
|
||||
import com.artemchep.keyguard.common.usecase.ShowMessage
|
||||
import com.artemchep.keyguard.common.util.flow.combineToList
|
||||
import com.artemchep.keyguard.common.util.flow.persistingStateIn
|
||||
import com.artemchep.keyguard.feature.add.AddStateItem
|
||||
import com.artemchep.keyguard.feature.add.AddStateOwnership
|
||||
import com.artemchep.keyguard.feature.add.LocalStateItem
|
||||
import com.artemchep.keyguard.feature.auth.common.SwitchFieldModel
|
||||
import com.artemchep.keyguard.feature.auth.common.TextFieldModel2
|
||||
import com.artemchep.keyguard.feature.auth.common.util.validatedTitle
|
||||
import com.artemchep.keyguard.feature.confirmation.organization.OrganizationConfirmationResult
|
||||
import com.artemchep.keyguard.feature.confirmation.organization.OrganizationConfirmationRoute
|
||||
import com.artemchep.keyguard.feature.home.vault.add.AddRoute
|
||||
import com.artemchep.keyguard.feature.home.vault.add.TmpIdentity
|
||||
import com.artemchep.keyguard.feature.home.vault.add.createItem
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
import com.artemchep.keyguard.feature.navigation.registerRouteResultReceiver
|
||||
import com.artemchep.keyguard.feature.navigation.state.PersistedStorage
|
||||
import com.artemchep.keyguard.feature.navigation.state.RememberStateFlowScope
|
||||
import com.artemchep.keyguard.feature.navigation.state.copy
|
||||
import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
||||
import com.artemchep.keyguard.feature.send.view.SendViewRoute
|
||||
import com.artemchep.keyguard.platform.parcelize.LeParcelable
|
||||
import com.artemchep.keyguard.platform.parcelize.LeParcelize
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import org.kodein.di.compose.localDI
|
||||
import org.kodein.di.direct
|
||||
import org.kodein.di.instance
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
@LeParcelize
|
||||
data class SendAddOwnershipData(
|
||||
val accountId: String?,
|
||||
) : LeParcelable
|
||||
|
||||
data class TmpText(
|
||||
val text: AddStateItem.Text<CreateSendRequest>,
|
||||
val items: List<AddStateItem>,
|
||||
)
|
||||
|
||||
data class TmpOptions(
|
||||
val deletionDate: AddStateItem.DateTime<CreateSendRequest>,
|
||||
val expirationDate: AddStateItem.DateTime<CreateSendRequest>,
|
||||
val password: AddStateItem.Password<CreateSendRequest>,
|
||||
val items: List<AddStateItem>,
|
||||
)
|
||||
|
||||
data class TmpNote(
|
||||
val note: AddStateItem.Note<CreateSendRequest>,
|
||||
val items: List<AddStateItem>,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun produceSendAddScreenState(
|
||||
args: SendAddRoute.Args,
|
||||
) = with(localDI().direct) {
|
||||
produceSendAddScreenState(
|
||||
args = args,
|
||||
getAccounts = instance(),
|
||||
getProfiles = instance(),
|
||||
getOrganizations = instance(),
|
||||
getCollections = instance(),
|
||||
getFolders = instance(),
|
||||
getCiphers = instance(),
|
||||
getSends = instance(),
|
||||
getTotpCode = instance(),
|
||||
getGravatarUrl = instance(),
|
||||
getMarkdown = instance(),
|
||||
logRepository = instance(),
|
||||
clipboardService = instance(),
|
||||
cipherUnsecureUrlCheck = instance(),
|
||||
showMessage = instance(),
|
||||
addSend = instance(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun produceSendAddScreenState(
|
||||
args: SendAddRoute.Args,
|
||||
getAccounts: GetAccounts,
|
||||
getProfiles: GetProfiles,
|
||||
getOrganizations: GetOrganizations,
|
||||
getCollections: GetCollections,
|
||||
getFolders: GetFolders,
|
||||
getCiphers: GetCiphers,
|
||||
getSends: GetSends,
|
||||
getTotpCode: GetTotpCode,
|
||||
getGravatarUrl: GetGravatarUrl,
|
||||
getMarkdown: GetMarkdown,
|
||||
logRepository: LogRepository,
|
||||
clipboardService: ClipboardService,
|
||||
cipherUnsecureUrlCheck: CipherUnsecureUrlCheck,
|
||||
showMessage: ShowMessage,
|
||||
addSend: AddSend,
|
||||
): Loadable<SendAddState> = produceScreenState(
|
||||
key = "send_add",
|
||||
initial = Loadable.Loading,
|
||||
args = arrayOf(
|
||||
args,
|
||||
getAccounts,
|
||||
getOrganizations,
|
||||
getCollections,
|
||||
getFolders,
|
||||
getCiphers,
|
||||
getTotpCode,
|
||||
),
|
||||
) {
|
||||
val copyText = copy(clipboardService)
|
||||
val markdown = getMarkdown().first()
|
||||
|
||||
val title = if (args.ownershipRo) {
|
||||
translate(Res.strings.addsend_header_edit_title)
|
||||
} else {
|
||||
translate(Res.strings.addsend_header_new_title)
|
||||
}
|
||||
|
||||
val ownershipFlow = produceOwnershipFlow(
|
||||
args = args,
|
||||
getProfiles = getProfiles,
|
||||
getSends = getSends,
|
||||
)
|
||||
|
||||
val textHolder = produceTextState(
|
||||
args = args,
|
||||
)
|
||||
val noteHolder = produceNoteState(
|
||||
args = args,
|
||||
markdown = markdown,
|
||||
)
|
||||
|
||||
val typeFlow = kotlin.run {
|
||||
val initialValue = args.type
|
||||
?: args.initialValue?.type
|
||||
// this should never happen
|
||||
?: DSend.Type.None
|
||||
flowOf(initialValue)
|
||||
}
|
||||
val typeItemsFlow = typeFlow
|
||||
.flatMapLatest { type ->
|
||||
when (type) {
|
||||
DSend.Type.Text -> flowOf(textHolder.items)
|
||||
DSend.Type.File -> flowOf(emptyList())
|
||||
DSend.Type.None -> flowOf(emptyList())
|
||||
}
|
||||
}
|
||||
val miscItems by lazy {
|
||||
listOf(
|
||||
AddStateItem.Section(
|
||||
id = "misc",
|
||||
),
|
||||
AddStateItem.Note(
|
||||
id = "misc.note",
|
||||
state = noteHolder.note.state,
|
||||
markdown = markdown,
|
||||
),
|
||||
)
|
||||
}
|
||||
val miscFlow = typeFlow
|
||||
.map { _ ->
|
||||
val hasNotes = false
|
||||
hasNotes
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.map { hasNotes ->
|
||||
miscItems.takeUnless { hasNotes }.orEmpty()
|
||||
}
|
||||
|
||||
val titleItem = AddStateItem.Title<CreateSendRequest>(
|
||||
id = "title",
|
||||
state = LocalStateItem(
|
||||
flow = kotlin.run {
|
||||
val key = "title"
|
||||
val sink = mutablePersistedFlow(key) {
|
||||
args.initialValue?.name
|
||||
?: ""
|
||||
}
|
||||
val state = asComposeState<String>(key)
|
||||
combine(
|
||||
sink
|
||||
.validatedTitle(this),
|
||||
typeFlow,
|
||||
) { validatedTitle, type ->
|
||||
TextFieldModel2.of(
|
||||
state = state,
|
||||
hint = translate(type.titleH()),
|
||||
validated = validatedTitle,
|
||||
onChange = state::value::set,
|
||||
)
|
||||
}
|
||||
.persistingStateIn(
|
||||
scope = screenScope,
|
||||
started = SharingStarted.WhileSubscribed(1000L),
|
||||
initialValue = TextFieldModel2.empty,
|
||||
)
|
||||
},
|
||||
populator = { field ->
|
||||
copy(
|
||||
title = field.text,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
val items1 = listOfNotNull(
|
||||
titleItem,
|
||||
)
|
||||
|
||||
fun stetify(flow: Flow<List<AddStateItem>>) = flow
|
||||
.map { items ->
|
||||
items
|
||||
.mapNotNull { item ->
|
||||
val stateHolder = item as? AddStateItem.HasState<Any?, CreateSendRequest>
|
||||
?: return@mapNotNull null
|
||||
|
||||
val state = stateHolder.state
|
||||
val flow = state.flow
|
||||
.map { v ->
|
||||
state.populator
|
||||
.partially2(v)
|
||||
}
|
||||
item.id to flow
|
||||
}
|
||||
}
|
||||
|
||||
val itfff = combine(
|
||||
typeItemsFlow,
|
||||
miscFlow,
|
||||
) { arr ->
|
||||
arr.toList().flatten()
|
||||
}
|
||||
.onEach { l ->
|
||||
logRepository.post("Foo3", "combine ${l.size}")
|
||||
}
|
||||
|
||||
val outputFlow = combine(
|
||||
stetify(itfff),
|
||||
stetify(flowOf(items1)),
|
||||
) { arr ->
|
||||
arr
|
||||
.flatMap {
|
||||
it
|
||||
}
|
||||
}
|
||||
.flatMapLatest { populatorFlows ->
|
||||
val typePopulator =
|
||||
typeFlow
|
||||
.map { type ->
|
||||
val f = fun(r: CreateSendRequest): CreateSendRequest {
|
||||
return r.copy(type = type)
|
||||
}
|
||||
f
|
||||
}
|
||||
val ownershipPopulator =
|
||||
ownershipFlow
|
||||
.map { ownership ->
|
||||
val f = fun(r: CreateSendRequest): CreateSendRequest {
|
||||
val requestOwnership = CreateSendRequest.Ownership(
|
||||
accountId = ownership.data.accountId,
|
||||
)
|
||||
return r.copy(ownership = requestOwnership)
|
||||
}
|
||||
f
|
||||
}
|
||||
(populatorFlows.map { it.second } + ownershipPopulator + typePopulator)
|
||||
.combineToList()
|
||||
}
|
||||
.map { populators ->
|
||||
populators.fold(
|
||||
initial = CreateSendRequest(
|
||||
now = Clock.System.now(),
|
||||
),
|
||||
) { y, x -> x(y) }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
|
||||
val items = listOf(
|
||||
AddStateItem.Title<CreateSendRequest>(
|
||||
id = "title",
|
||||
state = LocalStateItem(
|
||||
flow = MutableStateFlow(TextFieldModel2(mutableStateOf(""))),
|
||||
),
|
||||
),
|
||||
AddStateItem.Text<CreateSendRequest>(
|
||||
id = "text",
|
||||
state = LocalStateItem(
|
||||
flow = MutableStateFlow(
|
||||
value = AddStateItem.Text.State(
|
||||
value = TextFieldModel2.empty,
|
||||
label = "Text",
|
||||
singleLine = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AddStateItem.Switch<CreateSendRequest>(
|
||||
id = "text_switch",
|
||||
title = "Conceal text by default",
|
||||
state = LocalStateItem(
|
||||
flow = MutableStateFlow(
|
||||
value = SwitchFieldModel(
|
||||
checked = false,
|
||||
onChange = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
combine(
|
||||
ownershipFlow,
|
||||
outputFlow,
|
||||
itfff,
|
||||
) { ownership, output, ddd ->
|
||||
val state = SendAddState(
|
||||
title = title,
|
||||
ownership = ownership,
|
||||
items = items1 + ddd,
|
||||
onSave = {
|
||||
val request = CreateSendRequest(
|
||||
ownership = CreateSendRequest.Ownership(
|
||||
accountId = ownership.data.accountId,
|
||||
),
|
||||
title = "title",
|
||||
note = "note",
|
||||
// types
|
||||
type = DSend.Type.Text,
|
||||
text = CreateSendRequest.Text(
|
||||
text = "text",
|
||||
),
|
||||
// other
|
||||
now = Clock.System.now(),
|
||||
)
|
||||
val sendIdToRequestMap = mapOf(
|
||||
args.initialValue?.id?.takeIf { args.ownershipRo } to request,
|
||||
)
|
||||
addSend(sendIdToRequestMap)
|
||||
.effectTap {
|
||||
val intent = kotlin.run {
|
||||
val list = mutableListOf<NavigationIntent>()
|
||||
list += NavigationIntent.PopById(screenId, exclusive = false)
|
||||
if (true) { // TODO: args.behavior.launchEditedCipher
|
||||
val sendId = it.first()
|
||||
val accountId = ownership.data.accountId!!
|
||||
val route = SendViewRoute(
|
||||
sendId = sendId,
|
||||
accountId = accountId,
|
||||
)
|
||||
list += NavigationIntent.NavigateToRoute(route)
|
||||
}
|
||||
NavigationIntent.Composite(
|
||||
list = list,
|
||||
)
|
||||
}
|
||||
navigate(intent)
|
||||
}
|
||||
.launchIn(appScope)
|
||||
},
|
||||
)
|
||||
Loadable.Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun RememberStateFlowScope.produceOwnershipFlow(
|
||||
args: SendAddRoute.Args,
|
||||
getProfiles: GetProfiles,
|
||||
getSends: GetSends,
|
||||
): Flow<SendAddState.Ownership> {
|
||||
val ro = args.ownershipRo
|
||||
|
||||
data class Fool<T>(
|
||||
val value: T,
|
||||
val element: AddStateOwnership.Element?,
|
||||
)
|
||||
|
||||
val disk = loadDiskHandle("new_send")
|
||||
val accountIdSink = mutablePersistedFlow<String?>(
|
||||
key = "ownership.account_id",
|
||||
storage = PersistedStorage.InDisk(disk),
|
||||
) { null }
|
||||
|
||||
val initialState = kotlin.run {
|
||||
if (args.initialValue != null) {
|
||||
return@run SendAddOwnershipData(
|
||||
accountId = args.initialValue.accountId,
|
||||
)
|
||||
}
|
||||
|
||||
// Make an account that has the most ciphers a
|
||||
// default account.
|
||||
val accountId = ioEffect {
|
||||
val accountIds = getProfiles().toIO().bind()
|
||||
.map { it.accountId() }
|
||||
.toSet()
|
||||
|
||||
fun String.takeIfAccountIdExists() = this
|
||||
.takeIf { id ->
|
||||
id in accountIds
|
||||
}
|
||||
|
||||
val lastAccountId = accountIdSink.value?.takeIfAccountIdExists()
|
||||
if (lastAccountId != null) {
|
||||
return@ioEffect lastAccountId
|
||||
}
|
||||
|
||||
val ciphers = getSends().toIO().bind()
|
||||
ciphers
|
||||
.asSequence()
|
||||
.groupBy { it.accountId }
|
||||
// the one that has the most sends
|
||||
.maxByOrNull { entry -> entry.value.size }
|
||||
// account id
|
||||
?.key
|
||||
?.takeIfAccountIdExists()
|
||||
}.attempt().bind().getOrNull()
|
||||
|
||||
SendAddOwnershipData(
|
||||
accountId = accountId,
|
||||
)
|
||||
}
|
||||
val sink = mutablePersistedFlow("ownership") {
|
||||
initialState
|
||||
}
|
||||
|
||||
// If we are creating a new item, then remember the
|
||||
// last selected account to pre-select it next time.
|
||||
// TODO: Remember only when an item is created.
|
||||
sink
|
||||
.map { it.accountId }
|
||||
.onEach(accountIdSink::value::set)
|
||||
.launchIn(screenScope)
|
||||
|
||||
val accountFlow = combine(
|
||||
sink
|
||||
.map { it.accountId }
|
||||
.distinctUntilChanged(),
|
||||
getProfiles(),
|
||||
) { accountId, profiles ->
|
||||
if (accountId == null) {
|
||||
val item = AddStateOwnership.Element.Item(
|
||||
key = "account.empty",
|
||||
title = translate(Res.strings.account_none),
|
||||
stub = true,
|
||||
)
|
||||
val el = AddStateOwnership.Element(
|
||||
readOnly = ro,
|
||||
items = listOf(item),
|
||||
)
|
||||
return@combine Fool(
|
||||
value = null,
|
||||
element = el,
|
||||
)
|
||||
}
|
||||
val profileOrNull = profiles
|
||||
.firstOrNull { it.accountId() == accountId }
|
||||
val el = AddStateOwnership.Element(
|
||||
readOnly = ro,
|
||||
items = listOfNotNull(profileOrNull)
|
||||
.map { account ->
|
||||
val key = "account.${account.accountId()}"
|
||||
AddStateOwnership.Element.Item(
|
||||
key = key,
|
||||
title = account.email,
|
||||
text = account.accountHost,
|
||||
accentColors = account.accentColor,
|
||||
)
|
||||
},
|
||||
)
|
||||
Fool(
|
||||
value = accountId,
|
||||
element = el,
|
||||
)
|
||||
}
|
||||
|
||||
return combine(
|
||||
accountFlow,
|
||||
) { (account) ->
|
||||
val flags = if (ro) {
|
||||
OrganizationConfirmationRoute.Args.RO_ACCOUNT
|
||||
} else {
|
||||
0
|
||||
} or
|
||||
OrganizationConfirmationRoute.Args.HIDE_ORGANIZATION or
|
||||
OrganizationConfirmationRoute.Args.HIDE_COLLECTION or
|
||||
OrganizationConfirmationRoute.Args.HIDE_FOLDER or
|
||||
OrganizationConfirmationRoute.Args.PREMIUM_ACCOUNT
|
||||
val data = SendAddState.Ownership.Data(
|
||||
accountId = account.value,
|
||||
)
|
||||
val ui = AddStateOwnership(
|
||||
account = account.element,
|
||||
onClick = {
|
||||
val route = registerRouteResultReceiver(
|
||||
route = OrganizationConfirmationRoute(
|
||||
args = OrganizationConfirmationRoute.Args(
|
||||
decor = OrganizationConfirmationRoute.Args.Decor(
|
||||
title = translate(Res.strings.save_to),
|
||||
icon = Icons.Outlined.AccountBox,
|
||||
),
|
||||
flags = flags,
|
||||
accountId = account.value,
|
||||
),
|
||||
),
|
||||
) { result ->
|
||||
if (result is OrganizationConfirmationResult.Confirm) {
|
||||
val newState = SendAddOwnershipData(
|
||||
accountId = result.accountId,
|
||||
)
|
||||
sink.value = newState
|
||||
}
|
||||
}
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
SendAddState.Ownership(
|
||||
data = data,
|
||||
ui = ui,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun RememberStateFlowScope.produceTextState(
|
||||
args: SendAddRoute.Args,
|
||||
): TmpText {
|
||||
val prefix = "identity"
|
||||
|
||||
suspend fun createItem(
|
||||
key: String,
|
||||
label: String? = null,
|
||||
initialValue: String? = null,
|
||||
singleLine: Boolean = false,
|
||||
autocompleteOptions: ImmutableList<String> = persistentListOf(),
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
lens: Optional<CreateSendRequest, String>,
|
||||
) = createItem<CreateSendRequest>(
|
||||
prefix = prefix,
|
||||
key = key,
|
||||
label = label,
|
||||
initialValue = initialValue,
|
||||
singleLine = singleLine,
|
||||
autocompleteOptions = autocompleteOptions,
|
||||
keyboardOptions = keyboardOptions,
|
||||
populator = {
|
||||
lens.set(this, it.value.text)
|
||||
},
|
||||
)
|
||||
|
||||
val txt = args.initialValue?.text
|
||||
val text = createItem(
|
||||
key = "text",
|
||||
label = translate(Res.strings.identity_title),
|
||||
initialValue = txt?.text,
|
||||
singleLine = true,
|
||||
lens = CreateSendRequest.text.text,
|
||||
)
|
||||
return TmpText(
|
||||
text = text,
|
||||
items = listOfNotNull<AddStateItem>(
|
||||
text,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun RememberStateFlowScope.produceOptionsState(
|
||||
args: SendAddRoute.Args,
|
||||
): TmpText {
|
||||
val prefix = "options"
|
||||
|
||||
suspend fun createItem(
|
||||
key: String,
|
||||
label: String? = null,
|
||||
initialValue: String? = null,
|
||||
singleLine: Boolean = false,
|
||||
autocompleteOptions: ImmutableList<String> = persistentListOf(),
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
lens: Optional<CreateSendRequest, String>,
|
||||
) = createItem<CreateSendRequest>(
|
||||
prefix = prefix,
|
||||
key = key,
|
||||
label = label,
|
||||
initialValue = initialValue,
|
||||
singleLine = singleLine,
|
||||
autocompleteOptions = autocompleteOptions,
|
||||
keyboardOptions = keyboardOptions,
|
||||
populator = {
|
||||
lens.set(this, it.value.text)
|
||||
},
|
||||
)
|
||||
|
||||
val txt = args.initialValue?.text
|
||||
val text = createItem(
|
||||
key = "text",
|
||||
label = translate(Res.strings.identity_title),
|
||||
initialValue = txt?.text,
|
||||
singleLine = true,
|
||||
lens = CreateSendRequest.text.text,
|
||||
)
|
||||
return TmpText(
|
||||
text = text,
|
||||
items = listOfNotNull<AddStateItem>(
|
||||
text,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun RememberStateFlowScope.produceNoteState(
|
||||
args: SendAddRoute.Args,
|
||||
markdown: Boolean,
|
||||
): TmpNote {
|
||||
val prefix = "notes"
|
||||
|
||||
val note = kotlin.run {
|
||||
val id = "$prefix.note"
|
||||
|
||||
val initialValue = args.initialValue?.notes
|
||||
|
||||
val sink = mutablePersistedFlow(id) { initialValue.orEmpty() }
|
||||
val state = mutableComposeState(sink)
|
||||
val stateItem = LocalStateItem<TextFieldModel2, CreateSendRequest>(
|
||||
flow = sink
|
||||
.map { value ->
|
||||
val model = TextFieldModel2(
|
||||
state = state,
|
||||
text = value,
|
||||
hint = "Add any notes about this item here",
|
||||
onChange = state::value::set,
|
||||
)
|
||||
model
|
||||
}
|
||||
.persistingStateIn(
|
||||
scope = screenScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = TextFieldModel2.empty,
|
||||
),
|
||||
populator = { state ->
|
||||
CreateSendRequest.note.set(this, state.text)
|
||||
},
|
||||
)
|
||||
AddStateItem.Note(
|
||||
id = id,
|
||||
state = stateItem,
|
||||
markdown = markdown,
|
||||
)
|
||||
}
|
||||
return TmpNote(
|
||||
note = note,
|
||||
items = listOfNotNull<AddStateItem>(
|
||||
note,
|
||||
),
|
||||
)
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
package com.artemchep.keyguard.feature.send.list
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@ -15,11 +17,14 @@ import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AccessTime
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material.icons.outlined.Key
|
||||
import androidx.compose.material.icons.outlined.PersonAdd
|
||||
import androidx.compose.material.pullrefresh.PullRefreshState
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalAbsoluteTonalElevation
|
||||
@ -50,7 +55,6 @@ import com.artemchep.keyguard.common.model.DSend
|
||||
import com.artemchep.keyguard.common.model.expiredFlow
|
||||
import com.artemchep.keyguard.feature.EmptySearchView
|
||||
import com.artemchep.keyguard.feature.home.vault.component.FlatItemLayout2
|
||||
import com.artemchep.keyguard.feature.home.vault.component.SearchTextField
|
||||
import com.artemchep.keyguard.feature.home.vault.component.Section
|
||||
import com.artemchep.keyguard.feature.home.vault.component.rememberSecretAccentColor
|
||||
import com.artemchep.keyguard.feature.home.vault.component.surfaceColorAtElevationSemi
|
||||
@ -74,8 +78,15 @@ import com.artemchep.keyguard.ui.AvatarBadgeIcon
|
||||
import com.artemchep.keyguard.ui.AvatarBuilder
|
||||
import com.artemchep.keyguard.ui.CollectedEffect
|
||||
import com.artemchep.keyguard.ui.Compose
|
||||
import com.artemchep.keyguard.ui.DefaultFab
|
||||
import com.artemchep.keyguard.ui.DefaultSelection
|
||||
import com.artemchep.keyguard.ui.DropdownMenuItemFlat
|
||||
import com.artemchep.keyguard.ui.DropdownMinWidth
|
||||
import com.artemchep.keyguard.ui.DropdownScopeImpl
|
||||
import com.artemchep.keyguard.ui.ExpandedIfNotEmptyForRow
|
||||
import com.artemchep.keyguard.ui.FabState
|
||||
import com.artemchep.keyguard.ui.FlatItem
|
||||
import com.artemchep.keyguard.ui.FlatItemAction
|
||||
import com.artemchep.keyguard.ui.FlatItemTextContent
|
||||
import com.artemchep.keyguard.ui.MediumEmphasisAlpha
|
||||
import com.artemchep.keyguard.ui.OptionsButton
|
||||
@ -83,6 +94,7 @@ import com.artemchep.keyguard.ui.ScaffoldLazyColumn
|
||||
import com.artemchep.keyguard.ui.focus.FocusRequester2
|
||||
import com.artemchep.keyguard.ui.focus.focusRequester2
|
||||
import com.artemchep.keyguard.ui.icons.ChevronIcon
|
||||
import com.artemchep.keyguard.ui.icons.IconBox
|
||||
import com.artemchep.keyguard.ui.icons.KeyguardNote
|
||||
import com.artemchep.keyguard.ui.icons.KeyguardView
|
||||
import com.artemchep.keyguard.ui.pulltosearch.PullToSearch
|
||||
@ -224,6 +236,12 @@ private fun SendScreenContent(
|
||||
pullRefreshState: PullRefreshState,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
) {
|
||||
val dp = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
if (state.primaryActions.isEmpty()) {
|
||||
dp.value = false
|
||||
}
|
||||
ScaffoldLazyColumn(
|
||||
modifier = modifier
|
||||
.pullRefresh(pullRefreshState)
|
||||
@ -263,7 +281,77 @@ private fun SendScreenContent(
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionState = run {
|
||||
val fabVisible = state.primaryActions.isNotEmpty()
|
||||
val fabState = if (fabVisible) {
|
||||
// If there's only one primary action, then there's no
|
||||
// need to show the dropdown.
|
||||
val onClick = run {
|
||||
val action =
|
||||
state.primaryActions.firstNotNullOfOrNull { it as? FlatItemAction }
|
||||
action?.onClick
|
||||
?.takeIf {
|
||||
val count = state.primaryActions
|
||||
.count { it is FlatItemAction }
|
||||
count == 1
|
||||
}
|
||||
} ?:
|
||||
// lambda
|
||||
{
|
||||
dp.value = true
|
||||
}
|
||||
FabState(
|
||||
onClick = onClick,
|
||||
model = null,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
rememberUpdatedState(newValue = fabState)
|
||||
},
|
||||
floatingActionButton = {
|
||||
DefaultFab(
|
||||
icon = {
|
||||
IconBox(main = Icons.Outlined.Add)
|
||||
|
||||
// Inject the dropdown popup to the bottom of the
|
||||
// content.
|
||||
val onDismissRequest = remember(dp) {
|
||||
// lambda
|
||||
{
|
||||
dp.value = false
|
||||
}
|
||||
}
|
||||
DropdownMenu(
|
||||
modifier = Modifier
|
||||
.widthIn(min = DropdownMinWidth),
|
||||
expanded = dp.value,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
val scope = DropdownScopeImpl(this, onDismissRequest = onDismissRequest)
|
||||
with(scope) {
|
||||
state.primaryActions.forEachIndexed { index, action ->
|
||||
DropdownMenuItemFlat(
|
||||
action = action,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(Res.strings.send_main_new_item_button),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
pullRefreshState = pullRefreshState,
|
||||
bottomBar = {
|
||||
val selectionOrNull = (state.content as? SendListState.Content.Items)?.selection
|
||||
DefaultSelection(
|
||||
state = selectionOrNull,
|
||||
)
|
||||
},
|
||||
overlay = {
|
||||
PullToSearch(
|
||||
modifier = Modifier
|
||||
@ -602,7 +690,33 @@ fun VaultSendItemText(
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
ChevronIcon()
|
||||
val showCheckbox = when {
|
||||
localState.selectableItemState.selecting -> true
|
||||
else -> false
|
||||
}
|
||||
Crossfade(
|
||||
modifier = Modifier
|
||||
.size(
|
||||
width = 36.dp,
|
||||
height = 36.dp,
|
||||
),
|
||||
targetState = showCheckbox,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (showCheckbox) {
|
||||
Checkbox(
|
||||
checked = localState.selectableItemState.selected,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
} else {
|
||||
ChevronIcon()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
|
@ -188,7 +188,7 @@ suspend fun <
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.map { items ->
|
||||
if (items.size <= 1 && collapse) {
|
||||
if (items.size <= 1 && collapse || items.isEmpty()) {
|
||||
// Do not show a single filter item.
|
||||
return@map emptyList<SendFilterItem>()
|
||||
}
|
||||
|
@ -0,0 +1,758 @@
|
||||
package com.artemchep.keyguard.feature.send.util
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Attachment
|
||||
import androidx.compose.material.icons.outlined.DeleteForever
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Email
|
||||
import androidx.compose.material.icons.outlined.Password
|
||||
import androidx.compose.material.icons.outlined.Remove
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import arrow.core.some
|
||||
import com.artemchep.keyguard.common.io.launchIn
|
||||
import com.artemchep.keyguard.common.model.DProfile
|
||||
import com.artemchep.keyguard.common.model.DSend
|
||||
import com.artemchep.keyguard.common.model.PatchSendRequest
|
||||
import com.artemchep.keyguard.common.usecase.PatchSendById
|
||||
import com.artemchep.keyguard.common.usecase.RemoveSendById
|
||||
import com.artemchep.keyguard.common.usecase.SendToolbox
|
||||
import com.artemchep.keyguard.common.util.StringComparatorIgnoreCase
|
||||
import com.artemchep.keyguard.feature.confirmation.ConfirmationResult
|
||||
import com.artemchep.keyguard.feature.confirmation.ConfirmationRoute
|
||||
import com.artemchep.keyguard.feature.navigation.NavigationIntent
|
||||
import com.artemchep.keyguard.feature.navigation.registerRouteResultReceiver
|
||||
import com.artemchep.keyguard.feature.navigation.state.RememberStateFlowScope
|
||||
import com.artemchep.keyguard.platform.util.isRelease
|
||||
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.Stub
|
||||
import com.artemchep.keyguard.ui.icons.icon
|
||||
import com.artemchep.keyguard.ui.icons.iconSmall
|
||||
import com.artemchep.keyguard.ui.selection.SelectionHandle
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
object SendUtil {
|
||||
context(RememberStateFlowScope)
|
||||
fun renameActionOrNull(
|
||||
patchSendById: PatchSendById,
|
||||
sends: List<DSend>,
|
||||
canEdit: Boolean,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
if (!canEdit) {
|
||||
return@run null
|
||||
}
|
||||
|
||||
val icon = icon(Icons.Outlined.Edit)
|
||||
val title = if (sends.size > 1) {
|
||||
translate(Res.strings.sends_action_change_names_title)
|
||||
} else {
|
||||
translate(Res.strings.sends_action_change_name_title)
|
||||
}
|
||||
FlatItemAction(
|
||||
leading = icon,
|
||||
title = title,
|
||||
onClick = {
|
||||
before?.invoke()
|
||||
|
||||
val items = sends
|
||||
.sorted()
|
||||
.map { cipher ->
|
||||
ConfirmationRoute.Args.Item.StringItem(
|
||||
key = cipher.id,
|
||||
value = cipher.name,
|
||||
title = cipher.name,
|
||||
type = ConfirmationRoute.Args.Item.StringItem.Type.Text,
|
||||
canBeEmpty = false,
|
||||
)
|
||||
}
|
||||
val route = registerRouteResultReceiver(
|
||||
route = ConfirmationRoute(
|
||||
args = ConfirmationRoute.Args(
|
||||
icon = icon,
|
||||
title = title,
|
||||
items = items,
|
||||
),
|
||||
),
|
||||
) { result ->
|
||||
if (result is ConfirmationResult.Confirm) {
|
||||
val request = createPatchRequestMultiple(result.data) { name ->
|
||||
val nameFixed = name as String
|
||||
PatchSendRequest.Data(
|
||||
name = nameFixed.some(),
|
||||
)
|
||||
}
|
||||
patchSendById(request)
|
||||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
val success = result is ConfirmationResult.Confirm
|
||||
after?.invoke(success)
|
||||
}
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun renameFileActionOrNull(
|
||||
patchSendById: PatchSendById,
|
||||
sends: List<DSend>,
|
||||
canEdit: Boolean,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
if (!canEdit) {
|
||||
return@run null
|
||||
}
|
||||
|
||||
val allFiles = sends.all { it.file != null }
|
||||
if (!allFiles) {
|
||||
return@run null
|
||||
}
|
||||
|
||||
// TODO: Seems like at this moment we can not change the file name
|
||||
if (isRelease) return@run null
|
||||
|
||||
val icon = iconSmall(Icons.Outlined.Attachment, Icons.Outlined.Edit)
|
||||
val title = if (sends.size > 1) {
|
||||
translate(Res.strings.sends_action_change_filenames_title)
|
||||
} else {
|
||||
translate(Res.strings.sends_action_change_filename_title)
|
||||
}
|
||||
FlatItemAction(
|
||||
leading = icon,
|
||||
title = title,
|
||||
onClick = {
|
||||
before?.invoke()
|
||||
|
||||
val items = sends
|
||||
.sorted()
|
||||
.map { cipher ->
|
||||
ConfirmationRoute.Args.Item.StringItem(
|
||||
key = cipher.id,
|
||||
value = cipher.file?.fileName.orEmpty(),
|
||||
title = cipher.name,
|
||||
type = ConfirmationRoute.Args.Item.StringItem.Type.Text,
|
||||
canBeEmpty = false,
|
||||
)
|
||||
}
|
||||
val route = registerRouteResultReceiver(
|
||||
route = ConfirmationRoute(
|
||||
args = ConfirmationRoute.Args(
|
||||
icon = icon,
|
||||
title = title,
|
||||
items = items,
|
||||
),
|
||||
),
|
||||
) { result ->
|
||||
if (result is ConfirmationResult.Confirm) {
|
||||
val request = createPatchRequestMultiple(result.data) { name ->
|
||||
val nameFixed = name as String
|
||||
PatchSendRequest.Data(
|
||||
fileName = nameFixed.some(),
|
||||
)
|
||||
}
|
||||
patchSendById(request)
|
||||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
val success = result is ConfirmationResult.Confirm
|
||||
after?.invoke(success)
|
||||
}
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun changePasswordActionOrNull(
|
||||
patchSendById: PatchSendById,
|
||||
sends: List<DSend>,
|
||||
canEdit: Boolean,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
if (!canEdit) {
|
||||
return@run null
|
||||
}
|
||||
|
||||
val hasPasswords = sends.any { it.hasPassword }
|
||||
|
||||
val icon = iconSmall(Icons.Outlined.Password)
|
||||
val title = if (hasPasswords) {
|
||||
if (sends.size > 1) {
|
||||
translate(Res.strings.sends_action_change_passwords_title)
|
||||
} else {
|
||||
translate(Res.strings.sends_action_change_password_title)
|
||||
}
|
||||
} else {
|
||||
if (sends.size > 1) {
|
||||
translate(Res.strings.sends_action_set_passwords_title)
|
||||
} else {
|
||||
translate(Res.strings.sends_action_set_password_title)
|
||||
}
|
||||
}
|
||||
FlatItemAction(
|
||||
leading = icon,
|
||||
title = title,
|
||||
onClick = {
|
||||
before?.invoke()
|
||||
|
||||
val items = sends
|
||||
.sorted()
|
||||
.map { send ->
|
||||
ConfirmationRoute.Args.Item.StringItem(
|
||||
key = send.id,
|
||||
value = "",
|
||||
title = send.name,
|
||||
type = ConfirmationRoute.Args.Item.StringItem.Type.Password,
|
||||
canBeEmpty = true, // so you can clear passwords
|
||||
)
|
||||
}
|
||||
val route = registerRouteResultReceiver(
|
||||
route = ConfirmationRoute(
|
||||
args = ConfirmationRoute.Args(
|
||||
icon = icon,
|
||||
title = title,
|
||||
message = if (hasPasswords) {
|
||||
null
|
||||
} else {
|
||||
translate(Res.strings.sends_action_set_password_confirmation_message)
|
||||
},
|
||||
items = items,
|
||||
),
|
||||
),
|
||||
) { result ->
|
||||
if (result is ConfirmationResult.Confirm) {
|
||||
val request = createPatchRequestMultiple(result.data) { password ->
|
||||
val passwordFixed = (password as String)
|
||||
.takeIf { it.isNotEmpty() }
|
||||
PatchSendRequest.Data(
|
||||
password = passwordFixed.some(),
|
||||
)
|
||||
}
|
||||
patchSendById(request)
|
||||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
val success = result is ConfirmationResult.Confirm
|
||||
after?.invoke(success)
|
||||
}
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun removePasswordActionOrNull(
|
||||
patchSendById: PatchSendById,
|
||||
sends: List<DSend>,
|
||||
canEdit: Boolean,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
if (!canEdit) {
|
||||
return@run null
|
||||
}
|
||||
|
||||
val hasPasswords = sends.any { it.hasPassword }
|
||||
if (!hasPasswords) {
|
||||
return@run null
|
||||
}
|
||||
|
||||
val icon = iconSmall(Icons.Outlined.Password, Icons.Outlined.Remove)
|
||||
val title = if (sends.size > 1) {
|
||||
translate(Res.strings.sends_action_remove_passwords_title)
|
||||
} else {
|
||||
translate(Res.strings.sends_action_remove_password_title)
|
||||
}
|
||||
FlatItemAction(
|
||||
leading = icon,
|
||||
title = title,
|
||||
onClick = {
|
||||
before?.invoke()
|
||||
|
||||
val route = registerRouteResultReceiver(
|
||||
route = ConfirmationRoute(
|
||||
args = ConfirmationRoute.Args(
|
||||
icon = icon(Icons.Outlined.Password, Icons.Outlined.Remove),
|
||||
title = if (sends.size > 1) {
|
||||
translate(Res.strings.sends_action_remove_passwords_confirmation_title)
|
||||
} else {
|
||||
translate(Res.strings.sends_action_remove_password_confirmation_title)
|
||||
},
|
||||
message = translate(Res.strings.sends_action_remove_password_confirmation_message),
|
||||
),
|
||||
),
|
||||
) { result ->
|
||||
if (result is ConfirmationResult.Confirm) {
|
||||
val request = createPatchRequestSingle(sends) {
|
||||
PatchSendRequest.Data(
|
||||
password = null.some(),
|
||||
)
|
||||
}
|
||||
patchSendById(request)
|
||||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
val success = result is ConfirmationResult.Confirm
|
||||
after?.invoke(success)
|
||||
}
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun showEmailActionOrNull(
|
||||
patchSendById: PatchSendById,
|
||||
sends: List<DSend>,
|
||||
canEdit: Boolean,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
if (!canEdit) {
|
||||
return@run null
|
||||
}
|
||||
val canShowEmail = sends.any { it.hideEmail }
|
||||
if (!canShowEmail) {
|
||||
return@run null
|
||||
}
|
||||
showEmailAction(
|
||||
patchSendById = patchSendById,
|
||||
sends = sends,
|
||||
before = before,
|
||||
after = after,
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun showEmailAction(
|
||||
patchSendById: PatchSendById,
|
||||
sends: List<DSend>,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
val icon = iconSmall(Icons.Outlined.Email, Icons.Outlined.Visibility)
|
||||
val title = translate(Res.strings.sends_action_show_email_title)
|
||||
FlatItemAction(
|
||||
leading = icon,
|
||||
title = title,
|
||||
onClick = {
|
||||
before?.invoke()
|
||||
|
||||
val request = createPatchRequestSingle(sends) {
|
||||
PatchSendRequest.Data(
|
||||
hideEmail = false.some(),
|
||||
)
|
||||
}
|
||||
patchSendById(request)
|
||||
.launchIn(appScope)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun hideEmailActionOrNull(
|
||||
patchSendById: PatchSendById,
|
||||
sends: List<DSend>,
|
||||
canEdit: Boolean,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
if (!canEdit) {
|
||||
return@run null
|
||||
}
|
||||
val canHideEmail = sends.any { !it.hideEmail }
|
||||
if (!canHideEmail) {
|
||||
return@run null
|
||||
}
|
||||
hideEmailAction(
|
||||
patchSendById = patchSendById,
|
||||
sends = sends,
|
||||
before = before,
|
||||
after = after,
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun hideEmailAction(
|
||||
patchSendById: PatchSendById,
|
||||
sends: List<DSend>,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
val icon = iconSmall(Icons.Outlined.Email, Icons.Outlined.VisibilityOff)
|
||||
val title = translate(Res.strings.sends_action_hide_email_title)
|
||||
FlatItemAction(
|
||||
leading = icon,
|
||||
title = title,
|
||||
onClick = {
|
||||
before?.invoke()
|
||||
|
||||
val request = createPatchRequestSingle(sends) {
|
||||
PatchSendRequest.Data(
|
||||
hideEmail = true.some(),
|
||||
)
|
||||
}
|
||||
patchSendById(request)
|
||||
.launchIn(appScope)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun enableActionOrNull(
|
||||
patchSendById: PatchSendById,
|
||||
sends: List<DSend>,
|
||||
canEdit: Boolean,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
if (!canEdit) {
|
||||
return@run null
|
||||
}
|
||||
val canEnable = sends.any { it.disabled }
|
||||
if (!canEnable) {
|
||||
return@run null
|
||||
}
|
||||
enableAction(
|
||||
patchSendById = patchSendById,
|
||||
sends = sends,
|
||||
before = before,
|
||||
after = after,
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun enableAction(
|
||||
patchSendById: PatchSendById,
|
||||
sends: List<DSend>,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
val icon = icon(Icons.Stub)
|
||||
val title = translate(Res.strings.sends_action_enable_title)
|
||||
val text = translate(Res.strings.sends_action_enable_text)
|
||||
FlatItemAction(
|
||||
leading = icon,
|
||||
title = title,
|
||||
text = text,
|
||||
onClick = {
|
||||
before?.invoke()
|
||||
|
||||
val route = registerRouteResultReceiver(
|
||||
route = ConfirmationRoute(
|
||||
args = ConfirmationRoute.Args(
|
||||
title = translate(Res.strings.sends_action_enable_confirmation_title),
|
||||
),
|
||||
),
|
||||
) { result ->
|
||||
if (result is ConfirmationResult.Confirm) {
|
||||
val request = createPatchRequestSingle(sends) {
|
||||
PatchSendRequest.Data(
|
||||
disabled = false.some(),
|
||||
)
|
||||
}
|
||||
patchSendById(request)
|
||||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
val success = result is ConfirmationResult.Confirm
|
||||
after?.invoke(success)
|
||||
}
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun disableActionOrNull(
|
||||
patchSendById: PatchSendById,
|
||||
sends: List<DSend>,
|
||||
canEdit: Boolean,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
if (!canEdit) {
|
||||
return@run null
|
||||
}
|
||||
val canDisable = sends.any { !it.disabled }
|
||||
if (!canDisable) {
|
||||
return@run null
|
||||
}
|
||||
disableAction(
|
||||
patchSendById = patchSendById,
|
||||
sends = sends,
|
||||
before = before,
|
||||
after = after,
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun disableAction(
|
||||
patchSendById: PatchSendById,
|
||||
sends: List<DSend>,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
val icon = icon(Icons.Stub)
|
||||
val title = translate(Res.strings.sends_action_disable_title)
|
||||
val text = translate(Res.strings.sends_action_disable_text)
|
||||
FlatItemAction(
|
||||
leading = icon,
|
||||
title = title,
|
||||
text = text,
|
||||
onClick = {
|
||||
before?.invoke()
|
||||
|
||||
val route = registerRouteResultReceiver(
|
||||
route = ConfirmationRoute(
|
||||
args = ConfirmationRoute.Args(
|
||||
title = translate(Res.strings.sends_action_disable_confirmation_title),
|
||||
),
|
||||
),
|
||||
) { result ->
|
||||
if (result is ConfirmationResult.Confirm) {
|
||||
val request = createPatchRequestSingle(sends) {
|
||||
PatchSendRequest.Data(
|
||||
disabled = true.some(),
|
||||
)
|
||||
}
|
||||
patchSendById(request)
|
||||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
val success = result is ConfirmationResult.Confirm
|
||||
after?.invoke(success)
|
||||
}
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun deleteActionOrNull(
|
||||
removeSendById: RemoveSendById,
|
||||
sends: List<DSend>,
|
||||
canDelete: Boolean,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
if (!canDelete) {
|
||||
return@run null
|
||||
}
|
||||
deleteAction(
|
||||
removeSendById = removeSendById,
|
||||
sends = sends,
|
||||
before = before,
|
||||
after = after,
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun deleteAction(
|
||||
removeSendById: RemoveSendById,
|
||||
sends: List<DSend>,
|
||||
before: (() -> Unit)? = null,
|
||||
after: ((Boolean) -> Unit)? = null,
|
||||
) = kotlin.run {
|
||||
val icon = icon(Icons.Outlined.DeleteForever)
|
||||
val title = translate(Res.strings.sends_action_delete_title)
|
||||
FlatItemAction(
|
||||
leading = icon,
|
||||
title = title,
|
||||
onClick = {
|
||||
before?.invoke()
|
||||
|
||||
val route = registerRouteResultReceiver(
|
||||
route = ConfirmationRoute(
|
||||
args = ConfirmationRoute.Args(
|
||||
icon = icon(Icons.Outlined.DeleteForever),
|
||||
title = translate(Res.strings.sends_action_delete_confirmation_title),
|
||||
),
|
||||
),
|
||||
) { result ->
|
||||
if (result is ConfirmationResult.Confirm) {
|
||||
val sendIds = sends
|
||||
.map { it.id }
|
||||
.toSet()
|
||||
removeSendById(sendIds)
|
||||
.launchIn(appScope)
|
||||
}
|
||||
|
||||
val success = result is ConfirmationResult.Confirm
|
||||
after?.invoke(success)
|
||||
}
|
||||
val intent = NavigationIntent.NavigateToRoute(route)
|
||||
navigate(intent)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<DSend>.sorted() = this
|
||||
.sortedWith(StringComparatorIgnoreCase { it.name })
|
||||
|
||||
private fun createPatchRequestSingle(
|
||||
sends: List<DSend>,
|
||||
factory: () -> PatchSendRequest.Data,
|
||||
): PatchSendRequest = kotlin.run {
|
||||
val data = factory()
|
||||
val patch = sends
|
||||
.associate {
|
||||
it.id to data
|
||||
}
|
||||
PatchSendRequest(
|
||||
patch = patch,
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T> createPatchRequestMultiple(
|
||||
data: Map<String, T>,
|
||||
factory: (T) -> PatchSendRequest.Data,
|
||||
): PatchSendRequest = kotlin.run {
|
||||
val patch = data
|
||||
.mapValues {
|
||||
factory(it.value)
|
||||
}
|
||||
PatchSendRequest(
|
||||
patch = patch,
|
||||
)
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun actions(
|
||||
toolbox: SendToolbox,
|
||||
sends: List<DSend>,
|
||||
canEdit: Boolean,
|
||||
) = kotlin.run {
|
||||
buildContextItems {
|
||||
section {
|
||||
this += renameActionOrNull(
|
||||
patchSendById = toolbox.patchSendById,
|
||||
sends = sends,
|
||||
canEdit = canEdit,
|
||||
)
|
||||
this += renameFileActionOrNull(
|
||||
patchSendById = toolbox.patchSendById,
|
||||
sends = sends,
|
||||
canEdit = canEdit,
|
||||
)
|
||||
}
|
||||
section {
|
||||
this += changePasswordActionOrNull(
|
||||
patchSendById = toolbox.patchSendById,
|
||||
sends = sends,
|
||||
canEdit = canEdit,
|
||||
)
|
||||
this += removePasswordActionOrNull(
|
||||
patchSendById = toolbox.patchSendById,
|
||||
sends = sends,
|
||||
canEdit = canEdit,
|
||||
)
|
||||
}
|
||||
section {
|
||||
this += showEmailActionOrNull(
|
||||
patchSendById = toolbox.patchSendById,
|
||||
sends = sends,
|
||||
canEdit = canEdit,
|
||||
)
|
||||
this += hideEmailActionOrNull(
|
||||
patchSendById = toolbox.patchSendById,
|
||||
sends = sends,
|
||||
canEdit = canEdit,
|
||||
)
|
||||
}
|
||||
section {
|
||||
this += enableActionOrNull(
|
||||
patchSendById = toolbox.patchSendById,
|
||||
sends = sends,
|
||||
canEdit = canEdit,
|
||||
)
|
||||
this += disableActionOrNull(
|
||||
patchSendById = toolbox.patchSendById,
|
||||
sends = sends,
|
||||
canEdit = canEdit,
|
||||
)
|
||||
this += deleteActionOrNull(
|
||||
removeSendById = toolbox.removeSendById,
|
||||
sends = sends,
|
||||
canDelete = canEdit,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Selection
|
||||
//
|
||||
|
||||
fun canEditFlow(
|
||||
profilesFlow: Flow<List<DProfile>>,
|
||||
canWriteFlow: Flow<Boolean>,
|
||||
) = kotlin.run {
|
||||
canWriteFlow
|
||||
.flatMapLatest { canWrite ->
|
||||
if (!canWrite) {
|
||||
return@flatMapLatest flowOf(false)
|
||||
}
|
||||
|
||||
// Using send requires a user to have the
|
||||
// Bitwarden premium. We don't want to show the
|
||||
// new item button if a user won't be able to
|
||||
// create an item anyway.
|
||||
profilesFlow
|
||||
.map { profiles ->
|
||||
profiles.any { it.premium }
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
context(RememberStateFlowScope)
|
||||
fun selectionFlow(
|
||||
selectionHandle: SelectionHandle,
|
||||
sendsFlow: Flow<List<DSend>>,
|
||||
canEditFlow: Flow<Boolean>,
|
||||
//
|
||||
toolbox: SendToolbox,
|
||||
) = combine(
|
||||
sendsFlow,
|
||||
canEditFlow,
|
||||
selectionHandle.idsFlow,
|
||||
) { sends, canEdit, selectedSendIds ->
|
||||
if (selectedSendIds.isEmpty()) {
|
||||
return@combine null
|
||||
}
|
||||
|
||||
val selectedSends = sends
|
||||
.filter { it.id in selectedSendIds }
|
||||
val actions = actions(
|
||||
toolbox = toolbox,
|
||||
sends = selectedSends,
|
||||
canEdit = canEdit,
|
||||
)
|
||||
Selection(
|
||||
count = selectedSends.size,
|
||||
actions = actions.toPersistentList(),
|
||||
onClear = selectionHandle::clearSelection,
|
||||
)
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import arrow.optics.optics
|
||||
import com.artemchep.keyguard.common.model.DSend
|
||||
import com.artemchep.keyguard.feature.home.vault.model.VaultItemIcon
|
||||
import com.artemchep.keyguard.feature.home.vault.model.VaultViewItem
|
||||
import com.artemchep.keyguard.ui.ContextItem
|
||||
import com.artemchep.keyguard.ui.FlatItemAction
|
||||
|
||||
@Immutable
|
||||
@ -27,7 +28,7 @@ data class SendViewState(
|
||||
val synced: Boolean,
|
||||
val onCopy: (() -> Unit)?,
|
||||
val onShare: (() -> Unit)?,
|
||||
val actions: List<FlatItemAction>,
|
||||
val actions: List<ContextItem>,
|
||||
val items: List<VaultViewItem>,
|
||||
) : Content {
|
||||
companion object;
|
||||
|
@ -57,10 +57,13 @@ import com.artemchep.keyguard.common.usecase.GetPasswordStrength
|
||||
import com.artemchep.keyguard.common.usecase.GetSends
|
||||
import com.artemchep.keyguard.common.usecase.GetWebsiteIcons
|
||||
import com.artemchep.keyguard.common.usecase.MoveCipherToFolderById
|
||||
import com.artemchep.keyguard.common.usecase.PatchSendById
|
||||
import com.artemchep.keyguard.common.usecase.RemoveAttachment
|
||||
import com.artemchep.keyguard.common.usecase.RemoveCipherById
|
||||
import com.artemchep.keyguard.common.usecase.RemoveSendById
|
||||
import com.artemchep.keyguard.common.usecase.RestoreCipherById
|
||||
import com.artemchep.keyguard.common.usecase.RetryCipher
|
||||
import com.artemchep.keyguard.common.usecase.SendToolbox
|
||||
import com.artemchep.keyguard.common.usecase.TrashCipherById
|
||||
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
|
||||
import com.artemchep.keyguard.feature.attachments.model.AttachmentItem
|
||||
@ -78,8 +81,10 @@ import com.artemchep.keyguard.feature.navigation.state.produceScreenState
|
||||
import com.artemchep.keyguard.feature.send.action.createSendActionOrNull
|
||||
import com.artemchep.keyguard.feature.send.action.createShareAction
|
||||
import com.artemchep.keyguard.feature.send.toVaultItemIcon
|
||||
import com.artemchep.keyguard.feature.send.util.SendUtil
|
||||
import com.artemchep.keyguard.res.Res
|
||||
import com.artemchep.keyguard.ui.FlatItemAction
|
||||
import com.artemchep.keyguard.ui.autoclose.launchAutoPopSelfHandler
|
||||
import com.artemchep.keyguard.ui.buildContextItems
|
||||
import com.artemchep.keyguard.ui.icons.ChevronIcon
|
||||
import com.artemchep.keyguard.ui.icons.IconBox
|
||||
@ -89,6 +94,7 @@ import com.artemchep.keyguard.ui.selection.selectionHandle
|
||||
import com.artemchep.keyguard.ui.text.annotate
|
||||
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||
import io.ktor.http.Url
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@ -120,18 +126,8 @@ fun sendViewScreenState(
|
||||
getAppIcons = instance(),
|
||||
getWebsiteIcons = instance(),
|
||||
getPasswordStrength = instance(),
|
||||
cipherUnsecureUrlCheck = instance(),
|
||||
cipherUnsecureUrlAutoFix = instance(),
|
||||
cipherFieldSwitchToggle = instance(),
|
||||
moveCipherToFolderById = instance(),
|
||||
changeCipherNameById = instance(),
|
||||
changeCipherPasswordById = instance(),
|
||||
retryCipher = instance(),
|
||||
copyCipherById = instance(),
|
||||
restoreCipherById = instance(),
|
||||
trashCipherById = instance(),
|
||||
removeCipherById = instance(),
|
||||
favouriteCipherById = instance(),
|
||||
toolbox = instance(),
|
||||
downloadManager = instance(),
|
||||
downloadAttachment = instance(),
|
||||
removeAttachment = instance(),
|
||||
@ -173,18 +169,8 @@ fun sendViewScreenState(
|
||||
getAppIcons: GetAppIcons,
|
||||
getWebsiteIcons: GetWebsiteIcons,
|
||||
getPasswordStrength: GetPasswordStrength,
|
||||
cipherUnsecureUrlCheck: CipherUnsecureUrlCheck,
|
||||
cipherUnsecureUrlAutoFix: CipherUnsecureUrlAutoFix,
|
||||
cipherFieldSwitchToggle: CipherFieldSwitchToggle,
|
||||
moveCipherToFolderById: MoveCipherToFolderById,
|
||||
changeCipherNameById: ChangeCipherNameById,
|
||||
changeCipherPasswordById: ChangeCipherPasswordById,
|
||||
retryCipher: RetryCipher,
|
||||
copyCipherById: CopyCipherById,
|
||||
restoreCipherById: RestoreCipherById,
|
||||
trashCipherById: TrashCipherById,
|
||||
removeCipherById: RemoveCipherById,
|
||||
favouriteCipherById: FavouriteCipherById,
|
||||
toolbox: SendToolbox,
|
||||
downloadManager: DownloadManager,
|
||||
downloadAttachment: DownloadAttachment,
|
||||
removeAttachment: RemoveAttachment,
|
||||
@ -241,6 +227,7 @@ fun sendViewScreenState(
|
||||
.firstOrNull { it.id == sendId && it.accountId == accountId }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
launchAutoPopSelfHandler(secretFlow)
|
||||
combine(
|
||||
accountFlow,
|
||||
secretFlow,
|
||||
@ -276,11 +263,17 @@ fun sendViewScreenState(
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val actions = SendUtil.actions(
|
||||
toolbox = toolbox,
|
||||
sends = listOf(secretOrNull),
|
||||
canEdit = canAddSecret,
|
||||
)
|
||||
SendViewState.Content.Cipher(
|
||||
data = secretOrNull,
|
||||
icon = icon,
|
||||
synced = true,
|
||||
actions = emptyList(),
|
||||
actions = actions,
|
||||
onCopy = if (info != null) {
|
||||
// lambda
|
||||
{
|
||||
@ -393,8 +386,7 @@ private fun RememberStateFlowScope.oh(
|
||||
emit(section)
|
||||
emit(url)
|
||||
|
||||
val password = send.password
|
||||
if (password != null) {
|
||||
if (send.hasPassword) {
|
||||
val w = VaultViewItem.Label(
|
||||
id = "url.password",
|
||||
text = AnnotatedString(
|
||||
|
@ -12,6 +12,7 @@ import com.artemchep.keyguard.core.store.DatabaseSyncer
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenCipher
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenCollection
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenFolder
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenOptionalStringNullable
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenOrganization
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenProfile
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenSend
|
||||
@ -25,6 +26,7 @@ import com.artemchep.keyguard.provider.bitwarden.api.builder.delete
|
||||
import com.artemchep.keyguard.provider.bitwarden.api.builder.get
|
||||
import com.artemchep.keyguard.provider.bitwarden.api.builder.post
|
||||
import com.artemchep.keyguard.provider.bitwarden.api.builder.put
|
||||
import com.artemchep.keyguard.provider.bitwarden.api.builder.removePassword
|
||||
import com.artemchep.keyguard.provider.bitwarden.api.builder.restore
|
||||
import com.artemchep.keyguard.provider.bitwarden.api.builder.sync
|
||||
import com.artemchep.keyguard.provider.bitwarden.api.builder.trash
|
||||
@ -33,11 +35,13 @@ import com.artemchep.keyguard.provider.bitwarden.crypto.BitwardenCr
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.BitwardenCrCta
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.BitwardenCrImpl
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.BitwardenCrKey
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.CryptoKey
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.appendOrganizationToken
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.appendProfileToken
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.appendSendToken
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.appendUserToken
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.decodeSymmetricOrThrow
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.encrypted
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.makeSendCryptoKey
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.transform
|
||||
import com.artemchep.keyguard.provider.bitwarden.entity.CipherEntity
|
||||
import com.artemchep.keyguard.provider.bitwarden.entity.CollectionEntity
|
||||
@ -47,6 +51,7 @@ import com.artemchep.keyguard.provider.bitwarden.entity.SyncProfile
|
||||
import com.artemchep.keyguard.provider.bitwarden.entity.SyncSends
|
||||
import com.artemchep.keyguard.provider.bitwarden.entity.request.CipherUpdate
|
||||
import com.artemchep.keyguard.provider.bitwarden.entity.request.FolderUpdate
|
||||
import com.artemchep.keyguard.provider.bitwarden.entity.request.SendUpdate
|
||||
import com.artemchep.keyguard.provider.bitwarden.entity.request.of
|
||||
import com.artemchep.keyguard.provider.bitwarden.sync.SyncManager
|
||||
import io.ktor.client.HttpClient
|
||||
@ -122,7 +127,6 @@ class SyncEngine(
|
||||
val now = Clock.System.now()
|
||||
val crypto = crypto(
|
||||
profile = response.profile,
|
||||
sends = response.sends.orEmpty(),
|
||||
)
|
||||
|
||||
fun getCodec(
|
||||
@ -135,7 +139,7 @@ class SyncEngine(
|
||||
val key = BitwardenCrKey.SendToken(sendId)
|
||||
BitwardenCrCta.BitwardenCrCtaEnv(
|
||||
key = key,
|
||||
encryptionType = CipherEncryptor.Type.AesCbc256_B64,
|
||||
encryptionType = envEncryptionType,
|
||||
)
|
||||
} else if (organizationId != null) {
|
||||
val key = BitwardenCrKey.OrganizationToken(organizationId)
|
||||
@ -156,6 +160,43 @@ class SyncEngine(
|
||||
)
|
||||
}
|
||||
|
||||
fun getCodecPair(
|
||||
mode: BitwardenCrCta.Mode,
|
||||
key: ByteArray,
|
||||
) = kotlin.run {
|
||||
val itemCrypto = kotlin.run {
|
||||
val symmetricCryptoKey = key
|
||||
.let(cryptoGenerator::makeSendCryptoKey)
|
||||
.let(CryptoKey.Companion::decodeSymmetricOrThrow)
|
||||
val cryptoKey = BitwardenCrKey.CryptoKey(
|
||||
symmetricCryptoKey = symmetricCryptoKey,
|
||||
)
|
||||
val cryptoKeyEnv = BitwardenCrCta.BitwardenCrCtaEnv(
|
||||
key = cryptoKey,
|
||||
)
|
||||
crypto.cta(
|
||||
env = cryptoKeyEnv,
|
||||
mode = mode,
|
||||
)
|
||||
}
|
||||
val globalCrypto = getCodec(
|
||||
mode = mode,
|
||||
)
|
||||
itemCrypto to globalCrypto
|
||||
}
|
||||
|
||||
fun getCodecPairFromEncrypted(
|
||||
mode: BitwardenCrCta.Mode,
|
||||
keyCipherText: String,
|
||||
) = kotlin.run {
|
||||
val key = crypto.decoder(BitwardenCrKey.UserToken)(keyCipherText)
|
||||
.data
|
||||
getCodecPair(
|
||||
key = key,
|
||||
mode = mode,
|
||||
)
|
||||
}
|
||||
|
||||
//
|
||||
// Profile
|
||||
//
|
||||
@ -940,8 +981,28 @@ class SyncEngine(
|
||||
localReEncoder = { model ->
|
||||
model
|
||||
},
|
||||
localDecoder = { l, d ->
|
||||
Unit
|
||||
localDecoder = { local, remote ->
|
||||
val itemKey = requireNotNull(local.keyBase64)
|
||||
.let(base64Service::decode)
|
||||
val (
|
||||
itemCrypto,
|
||||
globalCrypto,
|
||||
) = getCodecPair(
|
||||
mode = BitwardenCrCta.Mode.ENCRYPT,
|
||||
key = itemKey,
|
||||
)
|
||||
val encryptedSend = local.transform(
|
||||
itemCrypto = itemCrypto,
|
||||
globalCrypto = globalCrypto,
|
||||
)
|
||||
with(cryptoGenerator) {
|
||||
with(base64Service) {
|
||||
SendUpdate.of(
|
||||
model = encryptedSend,
|
||||
key = itemKey,
|
||||
)
|
||||
}
|
||||
} to local
|
||||
},
|
||||
localDeleteById = { ids ->
|
||||
sendDao.transaction {
|
||||
@ -966,32 +1027,36 @@ class SyncEngine(
|
||||
remoteItems = response.sends.orEmpty(),
|
||||
remoteLens = SyncManager.Lens<SyncSends>(
|
||||
getId = { it.id },
|
||||
getRevisionDate = { Instant.DISTANT_FUTURE },
|
||||
getRevisionDate = { it.revisionDate },
|
||||
),
|
||||
remoteDecoder = { remote, local ->
|
||||
val codec = getCodec(
|
||||
val (
|
||||
itemCrypto,
|
||||
globalCrypto,
|
||||
) = getCodecPairFromEncrypted(
|
||||
mode = BitwardenCrCta.Mode.DECRYPT,
|
||||
sendId = remote.id,
|
||||
keyCipherText = remote.key,
|
||||
)
|
||||
val codec2 = getCodec(
|
||||
mode = BitwardenCrCta.Mode.DECRYPT,
|
||||
)
|
||||
codec
|
||||
itemCrypto
|
||||
.sendDecoder(
|
||||
entity = remote,
|
||||
codec2 = codec2,
|
||||
codec2 = globalCrypto,
|
||||
localSyncId = local?.sendId,
|
||||
)
|
||||
},
|
||||
remoteDeleteById = { id ->
|
||||
TODO()
|
||||
user.env.back().api.sends.focus(id).delete(
|
||||
httpClient = httpClient,
|
||||
env = env,
|
||||
token = user.token.accessToken,
|
||||
)
|
||||
},
|
||||
remoteDecodedFallback = { remote, localOrNull, e ->
|
||||
e.printStackTrace()
|
||||
val service = BitwardenService(
|
||||
remote = BitwardenService.Remote(
|
||||
id = remote.id,
|
||||
revisionDate = now,
|
||||
revisionDate = remote.revisionDate,
|
||||
deletedDate = null,
|
||||
),
|
||||
error = BitwardenService.Error(
|
||||
@ -1015,8 +1080,68 @@ class SyncEngine(
|
||||
)
|
||||
model
|
||||
},
|
||||
remotePut = {
|
||||
TODO()
|
||||
remotePut = { (r, local) ->
|
||||
val sendsApi = user.env.back().api.sends
|
||||
val sendResponse = when (r) {
|
||||
is SendUpdate.Modify -> {
|
||||
val cipherApi = sendsApi.focus(r.sendId)
|
||||
var cipherRequest = r.sendRequest
|
||||
|
||||
val hasChanged =
|
||||
r.source.service.remote?.revisionDate != r.source.revisionDate
|
||||
if (hasChanged) {
|
||||
val putCipher = cipherApi.put(
|
||||
httpClient = httpClient,
|
||||
env = env,
|
||||
token = user.token.accessToken,
|
||||
body = cipherRequest,
|
||||
)
|
||||
|
||||
// Removing a password has a separate endpoint and
|
||||
// requires an additional request.
|
||||
val shouldRemovePassword = r.source.changes?.passwordBase64
|
||||
?.let { it is BitwardenOptionalStringNullable.Some && it.value == null } == true
|
||||
if (shouldRemovePassword && putCipher.password != null) {
|
||||
cipherApi.removePassword(
|
||||
httpClient = httpClient,
|
||||
env = env,
|
||||
token = user.token.accessToken,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
cipherApi.get(
|
||||
httpClient = httpClient,
|
||||
env = env,
|
||||
token = user.token.accessToken,
|
||||
)
|
||||
}
|
||||
|
||||
is SendUpdate.Create -> {
|
||||
sendsApi.post(
|
||||
httpClient = httpClient,
|
||||
env = env,
|
||||
token = user.token.accessToken,
|
||||
body = r.sendRequest,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val itemKey = requireNotNull(local.keyBase64)
|
||||
.let(base64Service::decode)
|
||||
val (
|
||||
itemCrypto,
|
||||
globalCrypto,
|
||||
) = getCodecPair(
|
||||
mode = BitwardenCrCta.Mode.DECRYPT,
|
||||
key = itemKey,
|
||||
)
|
||||
itemCrypto
|
||||
.sendDecoder(
|
||||
entity = sendResponse,
|
||||
codec2 = globalCrypto,
|
||||
localSyncId = local.sendId,
|
||||
)
|
||||
},
|
||||
onLog = { msg, logLevel ->
|
||||
logRepository.post(TAG, msg, logLevel)
|
||||
@ -1030,7 +1155,6 @@ class SyncEngine(
|
||||
|
||||
private fun crypto(
|
||||
profile: SyncProfile,
|
||||
sends: List<SyncSends>,
|
||||
): BitwardenCr = kotlin.run {
|
||||
val builder = BitwardenCrImpl(
|
||||
cipherEncryptor = cipherEncryptor,
|
||||
@ -1054,13 +1178,6 @@ class SyncEngine(
|
||||
keyCipherText = organization.key,
|
||||
)
|
||||
}
|
||||
|
||||
sends.forEach { send ->
|
||||
appendSendToken(
|
||||
id = send.id,
|
||||
keyCipherText = send.key,
|
||||
)
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
|
@ -455,12 +455,37 @@ suspend fun ServerEnvApi.Sends.post(
|
||||
route = "post-send",
|
||||
)
|
||||
|
||||
suspend fun ServerEnvApi.Sends.Send.get(
|
||||
httpClient: HttpClient,
|
||||
env: ServerEnv,
|
||||
token: String,
|
||||
) = httpClient
|
||||
.get(url) {
|
||||
headers(env)
|
||||
header("Authorization", "Bearer $token")
|
||||
attributes.put(routeAttribute, "get-send")
|
||||
}
|
||||
.bodyOrApiException<SyncSends>()
|
||||
|
||||
suspend fun ServerEnvApi.Sends.Send.put(
|
||||
httpClient: HttpClient,
|
||||
env: ServerEnv,
|
||||
token: String,
|
||||
body: SendRequest,
|
||||
) = url.put<SendRequest, SyncSends>(
|
||||
httpClient = httpClient,
|
||||
env = env,
|
||||
token = token,
|
||||
body = body,
|
||||
route = "put-send",
|
||||
)
|
||||
|
||||
suspend fun ServerEnvApi.Sends.Send.removePassword(
|
||||
httpClient: HttpClient,
|
||||
env: ServerEnv,
|
||||
token: String,
|
||||
) = httpClient
|
||||
.put(url) {
|
||||
.put(removePassword) {
|
||||
headers(env)
|
||||
header("Authorization", "Bearer $token")
|
||||
attributes.put(routeAttribute, "remove-password-send")
|
||||
|
@ -27,6 +27,11 @@ sealed interface BitwardenCrKey {
|
||||
data class SendToken(
|
||||
val id: String,
|
||||
) : BitwardenCrKey
|
||||
|
||||
data class CryptoKey(
|
||||
val symmetricCryptoKey: SymmetricCryptoKey2? = null,
|
||||
val asymmetricCryptoKey: AsymmetricCryptoKey? = null,
|
||||
) : BitwardenCrKey
|
||||
}
|
||||
|
||||
interface BitwardenCr {
|
||||
@ -54,7 +59,7 @@ class BitwardenCrCta(
|
||||
data class BitwardenCrCtaEnv(
|
||||
val key: BitwardenCrKey,
|
||||
/** An encryption type */
|
||||
val encryptionType: CipherEncryptor.Type,
|
||||
val encryptionType: CipherEncryptor.Type = CipherEncryptor.Type.AesCbc256_HmacSha256_B64,
|
||||
)
|
||||
|
||||
fun withEnv(env: BitwardenCrCtaEnv) = BitwardenCrCta(
|
||||
@ -164,9 +169,27 @@ class BitwardenCrImpl(
|
||||
// Decoder
|
||||
//
|
||||
|
||||
override fun decoder(key: BitwardenCrKey): Decoder = decoders.getValue(key)
|
||||
override fun decoder(key: BitwardenCrKey): Decoder = when (key) {
|
||||
is BitwardenCrKey.CryptoKey -> {
|
||||
cipherEncryptor::decode2
|
||||
.partially2(key.symmetricCryptoKey)
|
||||
.partially2(key.asymmetricCryptoKey)
|
||||
.withExceptionHandling(
|
||||
key,
|
||||
symmetricCryptoKey = key.symmetricCryptoKey,
|
||||
)
|
||||
}
|
||||
else -> decoders.getValue(key)
|
||||
}
|
||||
|
||||
override fun encoder(key: BitwardenCrKey): Encoder = encoders.getValue(key)
|
||||
override fun encoder(key: BitwardenCrKey): Encoder = when (key) {
|
||||
is BitwardenCrKey.CryptoKey -> {
|
||||
cipherEncryptor::encode2
|
||||
.partially3(key.symmetricCryptoKey)
|
||||
.partially3(key.asymmetricCryptoKey)
|
||||
}
|
||||
else -> encoders.getValue(key)
|
||||
}
|
||||
|
||||
override fun cta(
|
||||
env: BitwardenCrCta.BitwardenCrCtaEnv,
|
||||
@ -299,37 +322,6 @@ fun BitwardenCrFactoryScope.appendOrganizationToken(
|
||||
appendEncoder(key, encoder)
|
||||
}
|
||||
|
||||
fun BitwardenCrFactoryScope.appendSendToken(
|
||||
id: String,
|
||||
keyCipherText: String,
|
||||
) {
|
||||
val symmetricCryptoKey = decoder(BitwardenCrKey.UserToken)(keyCipherText)
|
||||
.data
|
||||
.let { keyMaterial ->
|
||||
val key = cryptoGenerator.hkdf(
|
||||
seed = keyMaterial,
|
||||
salt = "bitwarden-send".toByteArray(),
|
||||
info = "send".toByteArray(),
|
||||
length = 64,
|
||||
)
|
||||
key
|
||||
}
|
||||
.let(CryptoKey::decodeSymmetricOrThrow)
|
||||
val key = BitwardenCrKey.SendToken(id)
|
||||
val decoder = cipherEncryptor::decode2
|
||||
.partially2(symmetricCryptoKey)
|
||||
.partially2(null)
|
||||
.withExceptionHandling(
|
||||
key,
|
||||
symmetricCryptoKey = symmetricCryptoKey,
|
||||
)
|
||||
val encoder = cipherEncryptor::encode2
|
||||
.partially3(symmetricCryptoKey)
|
||||
.partially3(null)
|
||||
appendDecoder(key, decoder)
|
||||
appendEncoder(key, encoder)
|
||||
}
|
||||
|
||||
fun BitwardenCrFactoryScope.appendOrganizationToken2(
|
||||
id: String,
|
||||
keyData: ByteArray,
|
||||
@ -349,3 +341,20 @@ fun BitwardenCrFactoryScope.appendOrganizationToken2(
|
||||
appendDecoder(key, decoder)
|
||||
appendEncoder(key, encoder)
|
||||
}
|
||||
|
||||
//
|
||||
// Sends
|
||||
//
|
||||
|
||||
fun CryptoGenerator.makeSendCryptoKeyMaterial() = seed(length = 16)
|
||||
|
||||
fun CryptoGenerator.makeSendCryptoKey(
|
||||
keyMaterial: ByteArray = makeSendCryptoKeyMaterial(),
|
||||
): ByteArray {
|
||||
return hkdf(
|
||||
seed = keyMaterial,
|
||||
salt = "bitwarden-send".toByteArray(),
|
||||
info = "send".toByteArray(),
|
||||
length = 64,
|
||||
)
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ fun List<BitwardenCipher.Login.Uri>.transform(
|
||||
fun BitwardenCipher.Login.Uri.transform(
|
||||
crypto: BitwardenCrCta,
|
||||
) = copy(
|
||||
uri = crypto.transformString(uri),
|
||||
uri = crypto.transformString(uri.orEmpty()),
|
||||
)
|
||||
|
||||
@JvmName("encryptListOfBitwardenCipherLoginFido2Credentials")
|
||||
|
@ -3,17 +3,17 @@ package com.artemchep.keyguard.provider.bitwarden.crypto
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenSend
|
||||
|
||||
fun BitwardenSend.transform(
|
||||
crypto: BitwardenCrCta,
|
||||
codec2: BitwardenCrCta,
|
||||
itemCrypto: BitwardenCrCta,
|
||||
globalCrypto: BitwardenCrCta,
|
||||
) = copy(
|
||||
// common
|
||||
// key is encoded with profile key
|
||||
keyBase64 = keyBase64?.let(codec2::transformBase64),
|
||||
name = crypto.transformString(name),
|
||||
notes = crypto.transformString(notes),
|
||||
keyBase64 = keyBase64?.let(globalCrypto::transformBase64),
|
||||
name = itemCrypto.transformString(name),
|
||||
notes = itemCrypto.transformString(notes),
|
||||
// types
|
||||
text = text?.transform(crypto),
|
||||
file = file?.transform(crypto),
|
||||
text = text?.transform(itemCrypto),
|
||||
file = file?.transform(itemCrypto),
|
||||
)
|
||||
|
||||
fun BitwardenSend.File.transform(
|
||||
@ -26,5 +26,5 @@ fun BitwardenSend.File.transform(
|
||||
fun BitwardenSend.Text.transform(
|
||||
crypto: BitwardenCrCta,
|
||||
) = copy(
|
||||
text = crypto.transformString(text),
|
||||
text = crypto.transformString(requireNotNull(text)),
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ fun LoginUriRequest.Companion.of(
|
||||
model: BitwardenCipher.Login.Uri,
|
||||
) = kotlin.run {
|
||||
LoginUriRequest(
|
||||
uri = model.uri.orEmpty(),
|
||||
uri = requireNotNull(model.uri) { "Login URI request must have a non-null URI!" },
|
||||
match = model.match?.let(UriMatchTypeEntity::of),
|
||||
)
|
||||
}
|
||||
|
@ -1,12 +1,20 @@
|
||||
package com.artemchep.keyguard.provider.bitwarden.entity.request
|
||||
|
||||
import com.artemchep.keyguard.common.service.crypto.CryptoGenerator
|
||||
import com.artemchep.keyguard.common.service.text.Base64Service
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenOptionalStringNullable
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenSend
|
||||
import com.artemchep.keyguard.provider.bitwarden.entity.SendTypeEntity
|
||||
import com.artemchep.keyguard.provider.bitwarden.entity.of
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SendRequest(
|
||||
@SerialName("key")
|
||||
val key: String,
|
||||
@SerialName("type")
|
||||
val type: SendTypeEntity,
|
||||
@SerialName("name")
|
||||
@ -33,27 +41,92 @@ data class SendRequest(
|
||||
val text: SendTextRequest?,
|
||||
@SerialName("file")
|
||||
val file: SendFileRequest?,
|
||||
)
|
||||
) {
|
||||
companion object
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SendTextRequest(
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
@SerialName("notes")
|
||||
val notes: String,
|
||||
@SerialName("password")
|
||||
val password: String? = null,
|
||||
val type: SendTypeEntity,
|
||||
)
|
||||
|
||||
@SerialName("text")
|
||||
val text: String,
|
||||
@SerialName("hidden")
|
||||
val hidden: Boolean,
|
||||
) {
|
||||
companion object
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SendFileRequest(
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
@SerialName("notes")
|
||||
val notes: String,
|
||||
@SerialName("password")
|
||||
val password: String? = null,
|
||||
val type: SendTypeEntity,
|
||||
)
|
||||
@SerialName("fileName")
|
||||
val fileName: String? = null,
|
||||
) {
|
||||
companion object
|
||||
}
|
||||
|
||||
context(CryptoGenerator, Base64Service)
|
||||
fun SendRequest.Companion.of(
|
||||
model: BitwardenSend,
|
||||
key: ByteArray,
|
||||
) = kotlin.run {
|
||||
val type = SendTypeEntity.of(model.type)
|
||||
val text = model.text
|
||||
?.let(SendTextRequest::of)
|
||||
val file = model.file
|
||||
?.let(SendFileRequest::of)
|
||||
val deletionDate = model.deletedDate
|
||||
?: Clock.System.now()
|
||||
|
||||
val passwordHashBase64 = when (val pwd = model.changes?.passwordBase64) {
|
||||
is BitwardenOptionalStringNullable.Some -> run {
|
||||
pwd.value
|
||||
// Bitwarden doesn't allow us to remove the password
|
||||
// within the PUT request.
|
||||
?: return@run null
|
||||
val password = decode(pwd.value)
|
||||
// Send the hash code of that password, instead of
|
||||
// sending the actual password.
|
||||
val passwordHash = pbkdf2(
|
||||
seed = password,
|
||||
salt = key,
|
||||
iterations = 100_000,
|
||||
)
|
||||
encodeToString(passwordHash)
|
||||
}
|
||||
|
||||
is BitwardenOptionalStringNullable.None,
|
||||
null,
|
||||
-> null
|
||||
}
|
||||
val keyBase64 = requireNotNull(model.keyBase64) { "Send request must have a cipher key!" }
|
||||
SendRequest(
|
||||
type = type,
|
||||
key = keyBase64,
|
||||
name = requireNotNull(model.name) { "Send request must have a non-null name!" },
|
||||
notes = model.notes,
|
||||
password = passwordHashBase64,
|
||||
disabled = model.disabled,
|
||||
hideEmail = model.hideEmail == true,
|
||||
deletionDate = deletionDate,
|
||||
expirationDate = model.expirationDate,
|
||||
maxAccessCount = model.maxAccessCount,
|
||||
text = text,
|
||||
file = file,
|
||||
)
|
||||
}
|
||||
|
||||
fun SendTextRequest.Companion.of(
|
||||
model: BitwardenSend.Text,
|
||||
) = kotlin.run {
|
||||
SendTextRequest(
|
||||
text = model.text.orEmpty(),
|
||||
hidden = model.hidden == true,
|
||||
)
|
||||
}
|
||||
|
||||
fun SendFileRequest.Companion.of(
|
||||
model: BitwardenSend.File,
|
||||
) = kotlin.run {
|
||||
SendFileRequest(
|
||||
fileName = model.fileName,
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,49 @@
|
||||
package com.artemchep.keyguard.provider.bitwarden.entity.request
|
||||
|
||||
import com.artemchep.keyguard.common.service.crypto.CryptoGenerator
|
||||
import com.artemchep.keyguard.common.service.text.Base64Service
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenSend
|
||||
|
||||
sealed interface SendUpdate {
|
||||
companion object;
|
||||
|
||||
val source: BitwardenSend
|
||||
|
||||
data class Modify(
|
||||
override val source: BitwardenSend,
|
||||
val sendId: String,
|
||||
val sendRequest: SendRequest,
|
||||
) : SendUpdate
|
||||
|
||||
data class Create(
|
||||
override val source: BitwardenSend,
|
||||
val sendRequest: SendRequest,
|
||||
) : SendUpdate
|
||||
}
|
||||
|
||||
context(CryptoGenerator, Base64Service)
|
||||
fun SendUpdate.Companion.of(
|
||||
model: BitwardenSend,
|
||||
key: ByteArray,
|
||||
) = when {
|
||||
model.service.remote != null -> {
|
||||
SendUpdate.Modify(
|
||||
source = model,
|
||||
sendId = model.service.remote.id,
|
||||
sendRequest = SendRequest.of(
|
||||
model = model,
|
||||
key = key,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
SendUpdate.Create(
|
||||
source = model,
|
||||
sendRequest = SendRequest.of(
|
||||
model = model,
|
||||
key = key,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package com.artemchep.keyguard.provider.bitwarden.mapper
|
||||
|
||||
import com.artemchep.keyguard.common.model.DSend
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenOptionalStringNullable
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenSend
|
||||
|
||||
suspend fun BitwardenSend.toDomain(
|
||||
@ -25,9 +26,14 @@ suspend fun BitwardenSend.toDomain(
|
||||
notes = notes.orEmpty(),
|
||||
accessCount = accessCount,
|
||||
maxAccessCount = maxAccessCount,
|
||||
hasPassword = changes?.passwordBase64
|
||||
?.let { it as? BitwardenOptionalStringNullable.Some }
|
||||
?.let { it.value != null }
|
||||
?: (password != null),
|
||||
synced = !service.deleted &&
|
||||
revisionDate == service.remote?.revisionDate,
|
||||
disabled = disabled,
|
||||
hideEmail = hideEmail ?: false,
|
||||
password = password,
|
||||
// types
|
||||
type = type,
|
||||
text = text?.toDomain(),
|
||||
|
@ -0,0 +1,209 @@
|
||||
package com.artemchep.keyguard.provider.bitwarden.usecase
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
import com.artemchep.keyguard.common.io.attempt
|
||||
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.DSend
|
||||
import com.artemchep.keyguard.common.model.canDelete
|
||||
import com.artemchep.keyguard.common.model.create.CreateRequest
|
||||
import com.artemchep.keyguard.common.model.create.CreateSendRequest
|
||||
import com.artemchep.keyguard.common.service.crypto.CryptoGenerator
|
||||
import com.artemchep.keyguard.common.service.text.Base64Service
|
||||
import com.artemchep.keyguard.common.usecase.AddFolder
|
||||
import com.artemchep.keyguard.common.usecase.AddSend
|
||||
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.BitwardenSend
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenService
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.makeSendCryptoKey
|
||||
import com.artemchep.keyguard.provider.bitwarden.crypto.makeSendCryptoKeyMaterial
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.util.ModifyDatabase
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.instance
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* @author Artem Chepurnyi
|
||||
*/
|
||||
class AddSendImpl(
|
||||
private val modifyDatabase: ModifyDatabase,
|
||||
private val cryptoGenerator: CryptoGenerator,
|
||||
private val base64Service: Base64Service,
|
||||
) : AddSend {
|
||||
companion object {
|
||||
private const val TAG = "AddSend.bitwarden"
|
||||
}
|
||||
|
||||
constructor(directDI: DirectDI) : this(
|
||||
modifyDatabase = directDI.instance(),
|
||||
cryptoGenerator = directDI.instance(),
|
||||
base64Service = directDI.instance(),
|
||||
)
|
||||
|
||||
override fun invoke(
|
||||
sendIdsToRequests: Map<String?, CreateSendRequest>,
|
||||
): IO<List<String>> = ioEffect {
|
||||
sendIdsToRequests
|
||||
}.flatMap { map ->
|
||||
modifyDatabase { database ->
|
||||
val dao = database.sendQueries
|
||||
val now = Clock.System.now()
|
||||
|
||||
val oldSendsMap = map
|
||||
.keys
|
||||
.filterNotNull()
|
||||
.mapNotNull { sendId ->
|
||||
dao
|
||||
.getBySendId(sendId)
|
||||
.executeAsOneOrNull()
|
||||
}
|
||||
.associateBy { it.sendId }
|
||||
|
||||
val models = map
|
||||
.map { (sendId, request) ->
|
||||
val old = oldSendsMap[sendId]?.data_
|
||||
BitwardenSend.of(
|
||||
cryptoGenerator = cryptoGenerator,
|
||||
base64Service = base64Service,
|
||||
now = now,
|
||||
request = request,
|
||||
old = old,
|
||||
)
|
||||
}
|
||||
if (models.isEmpty()) {
|
||||
return@modifyDatabase ModifyDatabase.Result(
|
||||
changedAccountIds = emptySet(),
|
||||
value = emptyList(),
|
||||
)
|
||||
}
|
||||
dao.transaction {
|
||||
models.forEach { send ->
|
||||
dao.insert(
|
||||
sendId = send.sendId,
|
||||
accountId = send.accountId,
|
||||
data = send,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val changedAccountIds = models
|
||||
.map { AccountId(it.accountId) }
|
||||
.toSet()
|
||||
ModifyDatabase.Result(
|
||||
changedAccountIds = changedAccountIds,
|
||||
value = models
|
||||
.map { it.sendId },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun BitwardenSend.Companion.of(
|
||||
cryptoGenerator: CryptoGenerator,
|
||||
base64Service: Base64Service,
|
||||
request: CreateSendRequest,
|
||||
now: Instant,
|
||||
old: BitwardenSend? = null,
|
||||
): BitwardenSend {
|
||||
val accountId = request.ownership?.accountId
|
||||
require(old?.service?.deleted != true) {
|
||||
"Can not modify deleted send!"
|
||||
}
|
||||
requireNotNull(accountId) { "Send must have an account!" }
|
||||
|
||||
val type = when (request.type) {
|
||||
DSend.Type.Text -> BitwardenSend.Type.Text
|
||||
DSend.Type.File -> BitwardenSend.Type.File
|
||||
null,
|
||||
DSend.Type.None,
|
||||
-> error("Send must have a type!")
|
||||
}
|
||||
|
||||
var text: BitwardenSend.Text? = null
|
||||
var file: BitwardenSend.File? = null
|
||||
when (type) {
|
||||
BitwardenSend.Type.Text -> {
|
||||
text = BitwardenSend.Text.of(
|
||||
request = request,
|
||||
old = old,
|
||||
)
|
||||
}
|
||||
|
||||
BitwardenSend.Type.File -> {
|
||||
file = BitwardenSend.File.of(
|
||||
request = request,
|
||||
)
|
||||
}
|
||||
|
||||
BitwardenSend.Type.None -> {
|
||||
error("Send must have a type!")
|
||||
}
|
||||
}
|
||||
|
||||
val keyBase64 = old?.keyBase64
|
||||
?: run {
|
||||
val key = cryptoGenerator.makeSendCryptoKeyMaterial()
|
||||
base64Service.encodeToString(key)
|
||||
}
|
||||
val cipherId = old?.sendId
|
||||
?: cryptoGenerator.uuid()
|
||||
val createdDate = old?.createdDate ?: request.now
|
||||
val deletedDate = old?.deletedDate ?: request.now.plus(with(Duration) {3L.days})
|
||||
return BitwardenSend(
|
||||
accountId = accountId,
|
||||
sendId = cipherId,
|
||||
accessId = "",
|
||||
revisionDate = now,
|
||||
createdDate = createdDate,
|
||||
deletedDate = deletedDate,
|
||||
// service fields
|
||||
service = BitwardenService(
|
||||
remote = old?.service?.remote,
|
||||
deleted = false,
|
||||
version = BitwardenService.VERSION,
|
||||
),
|
||||
// common
|
||||
keyBase64 = keyBase64,
|
||||
name = request.title,
|
||||
notes = request.note,
|
||||
accessCount = 0,
|
||||
maxAccessCount = null,
|
||||
password = null,
|
||||
disabled = false,
|
||||
hideEmail = null,
|
||||
// types
|
||||
type = type,
|
||||
text = text,
|
||||
file = file,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun BitwardenSend.Text.Companion.of(
|
||||
request: CreateSendRequest,
|
||||
old: BitwardenSend? = null,
|
||||
): BitwardenSend.Text {
|
||||
return BitwardenSend.Text(
|
||||
text = request.text.text,
|
||||
hidden = request.text.hidden,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun BitwardenSend.File.Companion.of(
|
||||
request: CreateSendRequest,
|
||||
): BitwardenSend.File {
|
||||
TODO()
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package com.artemchep.keyguard.provider.bitwarden.usecase
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.artemchep.keyguard.common.io.map
|
||||
import com.artemchep.keyguard.common.model.PatchSendRequest
|
||||
import com.artemchep.keyguard.common.service.text.Base64Service
|
||||
import com.artemchep.keyguard.common.usecase.PatchSendById
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenOptionalStringNullable
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenSend
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.util.ModifySendById
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
/**
|
||||
* @author Artem Chepurnyi
|
||||
*/
|
||||
class PatchSendByIdImpl(
|
||||
private val modifySendById: ModifySendById,
|
||||
private val base64Service: Base64Service,
|
||||
) : PatchSendById {
|
||||
companion object {
|
||||
private const val TAG = "PatchSendById.bitwarden"
|
||||
}
|
||||
|
||||
constructor(directDI: DirectDI) : this(
|
||||
modifySendById = directDI.instance(),
|
||||
base64Service = directDI.instance(),
|
||||
)
|
||||
|
||||
override fun invoke(
|
||||
patch: PatchSendRequest,
|
||||
) = performPatchSend(
|
||||
patch = patch,
|
||||
).map { true }
|
||||
|
||||
private fun performPatchSend(
|
||||
patch: PatchSendRequest,
|
||||
) = modifySendById(
|
||||
sendIds = patch.patch.keys,
|
||||
) { model ->
|
||||
val p = patch.patch.getValue(model.sendId)
|
||||
|
||||
val data = model.data_
|
||||
.run {
|
||||
val passwordBase64 = p.password
|
||||
.map { newPassword ->
|
||||
val newPasswordBase64 = newPassword
|
||||
?.let(base64Service::encodeToString)
|
||||
newPasswordBase64.let(BitwardenOptionalStringNullable::Some)
|
||||
}
|
||||
.getOrElse { BitwardenOptionalStringNullable.None }
|
||||
copy(
|
||||
name = p.name.getOrElse { name },
|
||||
hideEmail = p.hideEmail.getOrElse { hideEmail },
|
||||
disabled = p.disabled.getOrElse { disabled },
|
||||
file = p.fileName
|
||||
.map { fileName ->
|
||||
file?.copy(
|
||||
fileName = fileName,
|
||||
)
|
||||
}
|
||||
.getOrElse { file },
|
||||
changes = (changes ?: BitwardenSend.Changes()).copy(
|
||||
passwordBase64 = passwordBase64,
|
||||
),
|
||||
)
|
||||
}
|
||||
model.copy(data_ = data)
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package com.artemchep.keyguard.provider.bitwarden.usecase
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
import com.artemchep.keyguard.common.io.map
|
||||
import com.artemchep.keyguard.common.usecase.RemoveSendById
|
||||
import com.artemchep.keyguard.core.store.bitwarden.BitwardenSend
|
||||
import com.artemchep.keyguard.core.store.bitwarden.deleted
|
||||
import com.artemchep.keyguard.core.store.bitwarden.service
|
||||
import com.artemchep.keyguard.provider.bitwarden.usecase.util.ModifySendById
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
/**
|
||||
* @author Artem Chepurnyi
|
||||
*/
|
||||
class RemoveSendByIdImpl(
|
||||
private val modifySendById: ModifySendById,
|
||||
) : RemoveSendById {
|
||||
companion object {
|
||||
private const val TAG = "RemoveSendById.bitwarden"
|
||||
}
|
||||
|
||||
constructor(directDI: DirectDI) : this(
|
||||
modifySendById = directDI.instance(),
|
||||
)
|
||||
|
||||
override fun invoke(
|
||||
sendIds: Set<String>,
|
||||
): IO<Unit> = performRemoveSend(
|
||||
sendIds = sendIds,
|
||||
).map { Unit }
|
||||
|
||||
private fun performRemoveSend(
|
||||
sendIds: Set<String>,
|
||||
) = modifySendById(
|
||||
sendIds = sendIds,
|
||||
checkIfStub = false, // we want to be able to delete failed items
|
||||
) { model ->
|
||||
var new = model
|
||||
new = new.copy(
|
||||
data_ = BitwardenSend.service.deleted.set(new.data_, true),
|
||||
)
|
||||
new
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package com.artemchep.keyguard.provider.bitwarden.usecase.util
|
||||
|
||||
import com.artemchep.keyguard.common.io.IO
|
||||
import com.artemchep.keyguard.common.model.AccountId
|
||||
import com.artemchep.keyguard.data.bitwarden.Send
|
||||
import kotlinx.datetime.Clock
|
||||
import org.kodein.di.DirectDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
/**
|
||||
* @author Artem Chepurnyi
|
||||
*/
|
||||
class ModifySendById(
|
||||
private val modifyDatabase: ModifyDatabase,
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "ModifySendById.bitwarden"
|
||||
}
|
||||
|
||||
constructor(directDI: DirectDI) : this(
|
||||
modifyDatabase = directDI.instance(),
|
||||
)
|
||||
|
||||
operator fun invoke(
|
||||
sendIds: Set<String>,
|
||||
checkIfStub: Boolean = true,
|
||||
checkIfChanged: Boolean = true,
|
||||
updateRevisionDate: Boolean = true,
|
||||
transform: suspend (Send) -> Send,
|
||||
): IO<Set<String>> = modifyDatabase { database ->
|
||||
val dao = database.sendQueries
|
||||
val now = Clock.System.now()
|
||||
val models = dao
|
||||
.get()
|
||||
.executeAsList()
|
||||
.filter {
|
||||
it.sendId in sendIds
|
||||
}
|
||||
.mapNotNull { model ->
|
||||
// If the send was not properly decoded, then
|
||||
// prevent it from being pushed to backend.
|
||||
val service = model.data_.service
|
||||
if (checkIfStub && !service.canEdit()) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
var new = model
|
||||
new = transform(new)
|
||||
// If the cipher was not changed, then we do not need to
|
||||
// update it in the database.
|
||||
if (checkIfChanged && new == model) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
if (updateRevisionDate) {
|
||||
new = new.copy(
|
||||
data_ = new.data_.copy(
|
||||
revisionDate = now,
|
||||
),
|
||||
)
|
||||
}
|
||||
new
|
||||
}
|
||||
if (models.isEmpty()) {
|
||||
return@modifyDatabase ModifyDatabase.Result(
|
||||
changedAccountIds = emptySet(),
|
||||
value = emptySet(),
|
||||
)
|
||||
}
|
||||
dao.transaction {
|
||||
models.forEach { send ->
|
||||
dao.insert(
|
||||
sendId = send.sendId,
|
||||
accountId = send.accountId,
|
||||
data = send.data_,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val changedAccountIds = models
|
||||
.map { AccountId(it.accountId) }
|
||||
.toSet()
|
||||
val changedSendIds = models
|
||||
.map { it.sendId }
|
||||
.toSet()
|
||||
ModifyDatabase.Result(
|
||||
changedAccountIds = changedAccountIds,
|
||||
value = changedSendIds,
|
||||
)
|
||||
}
|
||||
}
|
@ -58,6 +58,7 @@
|
||||
<string name="postal_code">Postal code</string>
|
||||
<string name="pull_to_search">Pull to search</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="save_to">Save to</string>
|
||||
<string name="uri">URI</string>
|
||||
<string name="uris">URIs</string>
|
||||
<string name="note">Note</string>
|
||||
@ -248,9 +249,35 @@
|
||||
<string name="ciphers_action_trash_title">Trash</string>
|
||||
<string name="ciphers_action_restore_title">Restore</string>
|
||||
<string name="ciphers_action_delete_title">Delete forever</string>
|
||||
<string name="ciphers_action_delete_confirmation_title">Delete forever?</string>
|
||||
<string name="ciphers_action_configure_watchtower_alerts_title">Configure Watchtower alerts</string>
|
||||
<string name="ciphers_action_cascade_trash_associated_items_title">Move associated items to trash</string>
|
||||
|
||||
<string name="sends_action_change_name_title">Change name</string>
|
||||
<string name="sends_action_change_names_title">Change names</string>
|
||||
<string name="sends_action_change_filename_title">Change file name</string>
|
||||
<string name="sends_action_change_filenames_title">Change file names</string>
|
||||
<string name="sends_action_set_password_title">Set password</string>
|
||||
<string name="sends_action_set_passwords_title">Set passwords</string>
|
||||
<string name="sends_action_set_password_confirmation_message">Require a password for users to access this item</string>
|
||||
<string name="sends_action_change_password_title">Change password</string>
|
||||
<string name="sends_action_change_passwords_title">Change passwords</string>
|
||||
<string name="sends_action_remove_password_title">Remove password</string>
|
||||
<string name="sends_action_remove_passwords_title">Remove passwords</string>
|
||||
<string name="sends_action_remove_password_confirmation_title">Remove password?</string>
|
||||
<string name="sends_action_remove_passwords_confirmation_title">Remove passwords?</string>
|
||||
<string name="sends_action_remove_password_confirmation_message">Do not require a password for users to access this item</string>
|
||||
<string name="sends_action_hide_email_title">Hide email</string>
|
||||
<string name="sends_action_show_email_title">Show email</string>
|
||||
<string name="sends_action_enable_title">Activate</string>
|
||||
<string name="sends_action_enable_text">Active send is accessible by a public URL</string>
|
||||
<string name="sends_action_enable_confirmation_title">Activate?</string>
|
||||
<string name="sends_action_disable_title">Deactivate</string>
|
||||
<string name="sends_action_disable_text">Inactive send is inaccessible by a public URL</string>
|
||||
<string name="sends_action_disable_confirmation_title">Deactivate?</string>
|
||||
<string name="sends_action_delete_title">Delete forever</string>
|
||||
<string name="sends_action_delete_confirmation_title">Delete forever?</string>
|
||||
|
||||
<string name="file_action_open_with_title">Open with…</string>
|
||||
<string name="file_action_send_with_title">Send with…</string>
|
||||
<string name="file_action_open_in_file_manager_title">Open in a file manager</string>
|
||||
@ -577,6 +604,9 @@
|
||||
<string name="additem_auth_reprompt_title">Auth re-prompt</string>
|
||||
<string name="additem_auth_reprompt_text">Ask to authenticate again when you view or autofill a cipher</string>
|
||||
|
||||
<string name="addsend_header_new_title">New item</string>
|
||||
<string name="addsend_header_edit_title">Edit item</string>
|
||||
|
||||
<string name="addaccount_header_title">Add Bitwarden account</string>
|
||||
<string name="addaccount_disclaimer_bitwarden_label">We are not affiliated, associated, authorized, endorsed by, or in any way officially connected with the Bitwarden, Inc., or any of its subsidiaries or its affiliates.</string>
|
||||
<!--
|
||||
|
Loading…
x
Reference in New Issue
Block a user