feat(sync): перевёл группы синхронизации на Room и добавил контроль совместимости
This commit is contained in:
@@ -93,7 +93,7 @@ fun StorageHomeScreen(
|
||||
title = stringResource(R.string.storage_home_two_fa_title, uiState.twoFaCount),
|
||||
description = stringResource(R.string.storage_home_two_fa_subtitle),
|
||||
icon = Icons.Outlined.Lock,
|
||||
enabled = uiState.isAvailable,
|
||||
enabled = uiState.canManageDomainData,
|
||||
onClick = { onOpenTwoFa(uiState.storageUuid) },
|
||||
)
|
||||
|
||||
@@ -101,7 +101,7 @@ fun StorageHomeScreen(
|
||||
title = stringResource(R.string.storage_home_text_secrets_title, uiState.textSecretsCount),
|
||||
description = stringResource(R.string.storage_home_text_secrets_subtitle),
|
||||
icon = Icons.Outlined.Notes,
|
||||
enabled = uiState.isAvailable,
|
||||
enabled = uiState.canManageDomainData,
|
||||
onClick = { onOpenTextSecrets(uiState.storageUuid) },
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ data class StorageHomeScreenState(
|
||||
val isLoading: Boolean = true,
|
||||
val isAvailable: Boolean = false,
|
||||
val isEncrypted: Boolean = false,
|
||||
val isVirtualStorage: Boolean = false,
|
||||
val twoFaCount: Int = 0,
|
||||
val textSecretsCount: Int = 0,
|
||||
val canManageDomainData: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
)
|
||||
|
||||
@@ -44,15 +44,23 @@ class StorageHomeViewModel @Inject constructor(
|
||||
manageTwoFaTokensUseCase.observe(storage),
|
||||
manageTextSecretsUseCase.observe(storage),
|
||||
) { available, meta, twoFa, secrets ->
|
||||
val isRawEncrypted = meta.encInfo != null && !storage.isVirtualStorage
|
||||
val canManageDomainData = available && !isRawEncrypted
|
||||
state.value.copy(
|
||||
isLoading = false,
|
||||
storageUuid = storage.uuid.toString(),
|
||||
storageName = meta.name.orEmpty(),
|
||||
isAvailable = available,
|
||||
isEncrypted = meta.encInfo != null,
|
||||
isVirtualStorage = storage.isVirtualStorage,
|
||||
twoFaCount = twoFa.size,
|
||||
textSecretsCount = secrets.size,
|
||||
errorMessage = null,
|
||||
canManageDomainData = canManageDomainData,
|
||||
errorMessage = if (isRawEncrypted) {
|
||||
"Откройте расшифрованное отображение storage для работы с 2FA и секретами"
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}.collect { ui ->
|
||||
updateState(ui)
|
||||
|
||||
@@ -49,6 +49,16 @@ class TextSecretDetailsViewModel @Inject constructor(
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||
updateState(
|
||||
state.value.copy(
|
||||
isLoading = false,
|
||||
isAvailable = false,
|
||||
errorMessage = "Откройте расшифрованное отображение storage для просмотра и редактирования секрета",
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
combine(
|
||||
storage.isAvailable,
|
||||
manageTextSecretsUseCase.observe(storage).map { list ->
|
||||
@@ -76,6 +86,14 @@ class TextSecretDetailsViewModel @Inject constructor(
|
||||
fun delete(onDeleted: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
val storage = findStorageUseCase.find(storageUuid) ?: return@launch
|
||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||
updateState(
|
||||
state.value.copy(
|
||||
errorMessage = "Откройте расшифрованное отображение storage для просмотра и редактирования секрета",
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
val taskId = taskOrchestrator.enqueue(
|
||||
title = uiStrings(R.string.task_title_delete_text_secret),
|
||||
dispatcher = Dispatchers.IO,
|
||||
|
||||
@@ -42,6 +42,7 @@ fun TextSecretEditScreen(
|
||||
) {
|
||||
val uiState by viewModel.state.collectAsStateWithLifecycle()
|
||||
val currentOnSaved by rememberUpdatedState(onSaved)
|
||||
val inputEnabled = uiState.isAvailable && !uiState.isMutating && uiState.errorMessage == null
|
||||
|
||||
var title by remember(uiState.initialSecret) {
|
||||
mutableStateOf(uiState.initialSecret?.title.orEmpty())
|
||||
@@ -74,13 +75,16 @@ fun TextSecretEditScreen(
|
||||
stringResource(R.string.text_secret_edit)
|
||||
},
|
||||
)
|
||||
uiState.errorMessage?.let { err ->
|
||||
Text(text = err)
|
||||
}
|
||||
if (uiState.isMutating) {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
enabled = !uiState.isMutating,
|
||||
enabled = inputEnabled,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = { Text(stringResource(R.string.text_secret_title)) },
|
||||
)
|
||||
@@ -99,7 +103,7 @@ fun TextSecretEditScreen(
|
||||
onValueChange = { newLabel ->
|
||||
items[index] = item.copy(label = newLabel.ifBlank { null })
|
||||
},
|
||||
enabled = !uiState.isMutating,
|
||||
enabled = inputEnabled,
|
||||
modifier = Modifier.weight(0.45f),
|
||||
label = { Text(stringResource(R.string.text_secret_item_label_optional)) },
|
||||
)
|
||||
@@ -108,12 +112,12 @@ fun TextSecretEditScreen(
|
||||
onValueChange = { newValue ->
|
||||
items[index] = item.copy(value = newValue)
|
||||
},
|
||||
enabled = !uiState.isMutating,
|
||||
enabled = inputEnabled,
|
||||
modifier = Modifier.weight(0.55f),
|
||||
label = { Text(stringResource(R.string.text_secret_item_value)) },
|
||||
)
|
||||
IconButton(
|
||||
enabled = !uiState.isMutating,
|
||||
enabled = inputEnabled,
|
||||
onClick = {
|
||||
if (items.size > 1) {
|
||||
items.removeAt(index)
|
||||
@@ -131,7 +135,7 @@ fun TextSecretEditScreen(
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
TextButton(
|
||||
onClick = { items.add(TextSecretEntryRecord(label = null, value = "")) },
|
||||
enabled = !uiState.isMutating,
|
||||
enabled = inputEnabled,
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Text(stringResource(R.string.text_secret_add_item))
|
||||
@@ -144,7 +148,7 @@ fun TextSecretEditScreen(
|
||||
onSaved = currentOnSaved,
|
||||
)
|
||||
},
|
||||
enabled = title.isNotBlank() && !uiState.isMutating,
|
||||
enabled = title.isNotBlank() && inputEnabled,
|
||||
) {
|
||||
Text(stringResource(R.string.save))
|
||||
}
|
||||
|
||||
@@ -53,6 +53,16 @@ class TextSecretEditViewModel @Inject constructor(
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||
updateState(
|
||||
state.value.copy(
|
||||
isLoading = false,
|
||||
isAvailable = false,
|
||||
errorMessage = "Откройте расшифрованное отображение storage для редактирования секрета",
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
val initial = secretId?.let { id -> manageTextSecretsUseCase.get(storage, id) }
|
||||
combine(
|
||||
storage.isAvailable,
|
||||
@@ -83,6 +93,14 @@ class TextSecretEditViewModel @Inject constructor(
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val storage = findStorageUseCase.find(storageUuid) ?: return@launch
|
||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||
updateState(
|
||||
state.value.copy(
|
||||
errorMessage = "Откройте расшифрованное отображение storage для редактирования секрета",
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
val existingId = secretId
|
||||
val targetSecretId = existingId ?: UUID.randomUUID().toString()
|
||||
val taskId = taskOrchestrator.enqueue(
|
||||
|
||||
@@ -36,6 +36,16 @@ class TextSecretsViewModel @Inject constructor(
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||
updateState(
|
||||
state.value.copy(
|
||||
isLoading = false,
|
||||
isAvailable = false,
|
||||
errorMessage = "Откройте расшифрованное отображение storage для работы с секретами",
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
combine(
|
||||
storage.isAvailable,
|
||||
manageTextSecretsUseCase.observe(storage),
|
||||
|
||||
@@ -45,6 +45,16 @@ class TwoFaTokensViewModel @Inject constructor(
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||
updateState(
|
||||
state.value.copy(
|
||||
isLoading = false,
|
||||
isAvailable = false,
|
||||
errorMessage = "Откройте расшифрованное отображение storage для работы с 2FA",
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
combine(
|
||||
storage.isAvailable,
|
||||
manageTwoFaTokensUseCase.observe(storage),
|
||||
@@ -76,6 +86,14 @@ class TwoFaTokensViewModel @Inject constructor(
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val storage = findStorageUseCase.find(storageUuid) ?: return@launch
|
||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||
updateState(
|
||||
state.value.copy(
|
||||
errorMessage = "Откройте расшифрованное отображение storage для работы с 2FA",
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
taskOrchestrator.enqueue(
|
||||
title = uiStrings(R.string.task_title_save_2fa_token),
|
||||
dispatcher = Dispatchers.IO,
|
||||
@@ -109,6 +127,14 @@ class TwoFaTokensViewModel @Inject constructor(
|
||||
fun deleteToken(id: String) {
|
||||
viewModelScope.launch {
|
||||
val storage = findStorageUseCase.find(storageUuid) ?: return@launch
|
||||
if (storage.metaInfo.value.encInfo != null && !storage.isVirtualStorage) {
|
||||
updateState(
|
||||
state.value.copy(
|
||||
errorMessage = "Откройте расшифрованное отображение storage для работы с 2FA",
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
taskOrchestrator.enqueue(
|
||||
title = uiStrings(R.string.task_title_delete_2fa_token),
|
||||
dispatcher = Dispatchers.IO,
|
||||
|
||||
@@ -49,6 +49,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroupEncryptionKind
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
@@ -57,27 +58,6 @@ fun StorageSyncScreen(
|
||||
viewModel: StorageSyncViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
var pendingRemoveGroupId by remember { mutableStateOf<String?>(null) }
|
||||
var pendingRemoveStorage by remember { mutableStateOf<Pair<String, UUID>?>(null) }
|
||||
val pickerGroupId = state.pickerGroupId
|
||||
if (pickerGroupId != null) {
|
||||
StoragePickerScreen(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
groupId = pickerGroupId,
|
||||
onBack = viewModel::closePicker,
|
||||
onAddStorage = viewModel::addStorageToCurrentGroup,
|
||||
onToggleVault = viewModel::toggleVaultExpanded,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val storageByUuid = state.vaults
|
||||
.flatMap { vault -> flattenStorageTree(vault.storages) }
|
||||
.associateBy { it.uuid }
|
||||
|
||||
val groupEditLocked = state.isBusy || state.isStorageSyncRunning
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(state.userMessage) {
|
||||
@@ -96,6 +76,28 @@ fun StorageSyncScreen(
|
||||
viewModel.consumeUserMessage()
|
||||
}
|
||||
|
||||
var pendingRemoveGroupId by remember { mutableStateOf<String?>(null) }
|
||||
var pendingRemoveStorage by remember { mutableStateOf<Pair<String, UUID>?>(null) }
|
||||
val pickerGroupId = state.pickerGroupId
|
||||
if (pickerGroupId != null) {
|
||||
StoragePickerScreen(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
groupId = pickerGroupId,
|
||||
onBack = viewModel::closePicker,
|
||||
onAddStorage = viewModel::addStorageToCurrentGroup,
|
||||
onToggleVault = viewModel::toggleVaultExpanded,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val storageByUuid = state.vaults
|
||||
.flatMap { vault -> flattenStorageTree(vault.storages) }
|
||||
.associateBy { it.uuid }
|
||||
|
||||
val groupEditLocked = state.isBusy || state.isStorageSyncRunning
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
@@ -232,14 +234,24 @@ fun StorageSyncScreen(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
} else {
|
||||
val hasMixedEncryption = hasEncryptionMismatch(group, state.vaults)
|
||||
if (hasMixedEncryption) {
|
||||
if (group.incompatibleStorageUuids.isNotEmpty()) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.sync_group_mixed_encryption_warning),
|
||||
text = stringResource(
|
||||
id = R.string.sync_group_incompatible_warning,
|
||||
group.incompatibleStorageUuids.size,
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.sync_group_policy_line,
|
||||
groupPolicyLabel(group.encryptionKind),
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
group.storageUuids.forEach { storageUuid ->
|
||||
val storage = storageByUuid[storageUuid]
|
||||
@@ -426,12 +438,14 @@ private fun StoragePickerScreen(
|
||||
onBack: () -> Unit,
|
||||
onAddStorage: (UUID) -> Unit,
|
||||
onToggleVault: (UUID) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val selected = state.groups.firstOrNull { it.id == groupId }?.storageUuids ?: emptySet()
|
||||
val groupEditLocked = state.isBusy || state.isStorageSyncRunning
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) { inner ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -618,15 +632,11 @@ private fun flattenStorageTree(nodes: List<StorageSyncStorageUi>): List<StorageS
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasEncryptionMismatch(
|
||||
group: StorageSyncGroupUi,
|
||||
vaults: List<StorageSyncVaultUi>,
|
||||
): Boolean {
|
||||
if (group.storageUuids.isEmpty()) return false
|
||||
val byUuid = flattenStorageTree(vaults.flatMap { it.storages }).associateBy { it.uuid }
|
||||
val kinds = group.storageUuids.mapNotNull { byUuid[it]?.encryptionKind }.toSet()
|
||||
if (kinds.isEmpty()) return false
|
||||
val hasEncrypted = kinds.any { it != StorageSyncEncryptionKind.NotEncrypted }
|
||||
val hasPlain = kinds.contains(StorageSyncEncryptionKind.NotEncrypted)
|
||||
return hasEncrypted && hasPlain
|
||||
@Composable
|
||||
private fun groupPolicyLabel(kind: StorageSyncGroupEncryptionKind): String {
|
||||
return when (kind) {
|
||||
StorageSyncGroupEncryptionKind.UNSET -> stringResource(R.string.sync_group_policy_unset)
|
||||
StorageSyncGroupEncryptionKind.NONE -> stringResource(R.string.sync_group_policy_plain)
|
||||
StorageSyncGroupEncryptionKind.PASSWORD -> stringResource(R.string.sync_group_policy_password)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.github.nullptroma.wallenc.ui.screens.sync
|
||||
|
||||
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.StorageSyncGroupEncryptionKind
|
||||
import java.util.UUID
|
||||
|
||||
enum class StorageSyncEncryptionKind {
|
||||
@@ -28,6 +29,8 @@ data class StorageSyncVaultUi(
|
||||
data class StorageSyncGroupUi(
|
||||
val id: String,
|
||||
val storageUuids: Set<UUID>,
|
||||
val encryptionKind: StorageSyncGroupEncryptionKind = StorageSyncGroupEncryptionKind.UNSET,
|
||||
val incompatibleStorageUuids: Set<UUID> = emptySet(),
|
||||
)
|
||||
|
||||
data class StorageSyncScreenState(
|
||||
|
||||
@@ -10,8 +10,12 @@ import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
||||
import com.github.nullptroma.wallenc.usecases.AddStorageToSyncGroupResult
|
||||
import com.github.nullptroma.wallenc.usecases.ManageStorageSyncGroupsUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.StorageSyncCompatibilityInput
|
||||
import com.github.nullptroma.wallenc.usecases.isStorageCompatibleWithGroup
|
||||
import com.github.nullptroma.wallenc.usecases.storageEncryptionSecret
|
||||
import com.github.nullptroma.wallenc.vault.contract.DescribedVault
|
||||
import com.github.nullptroma.wallenc.vault.contract.VaultDescriptor
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -33,6 +37,7 @@ class StorageSyncViewModel @Inject constructor(
|
||||
private val taskOrchestrator: ITaskOrchestrator,
|
||||
private val uiStrings: UiStringResolver,
|
||||
) : ViewModelBase<StorageSyncScreenState>(StorageSyncScreenState()) {
|
||||
private var storageByUuid: Map<UUID, IStorage> = emptyMap()
|
||||
|
||||
init {
|
||||
refreshGroups()
|
||||
@@ -144,11 +149,40 @@ class StorageSyncViewModel @Inject constructor(
|
||||
val groupId = state.value.pickerGroupId ?: return
|
||||
viewModelScope.launch {
|
||||
withGroupMutationBusy {
|
||||
groupsUseCase.addStorageToGroup(groupId, storageUuid)
|
||||
UserNotification.TextRes(
|
||||
R.string.sync_msg_storage_added,
|
||||
listOf(groupId),
|
||||
val storage = storageByUuid[storageUuid]
|
||||
if (storage == null) {
|
||||
return@withGroupMutationBusy UserNotification.TextRes(R.string.sync_storage_not_in_vaults)
|
||||
}
|
||||
val isEncrypted = storage.metaInfo.value.encInfo != null
|
||||
val secret = vaultsManager.unlockManager
|
||||
.getOpenedStorageKey(storageUuid)
|
||||
?.let(::storageEncryptionSecret)
|
||||
val result = groupsUseCase.addStorageToGroup(
|
||||
groupId = groupId,
|
||||
storageUuid = storageUuid,
|
||||
compatibility = StorageSyncCompatibilityInput(
|
||||
isEncrypted = isEncrypted,
|
||||
encryptionSecret = secret,
|
||||
),
|
||||
)
|
||||
when (result) {
|
||||
AddStorageToSyncGroupResult.Added -> UserNotification.TextRes(
|
||||
R.string.sync_msg_storage_added,
|
||||
listOf(groupId),
|
||||
)
|
||||
AddStorageToSyncGroupResult.AlreadyInGroup -> UserNotification.TextRes(
|
||||
R.string.sync_msg_storage_already_added,
|
||||
)
|
||||
AddStorageToSyncGroupResult.MissingEncryptionSecret -> UserNotification.TextRes(
|
||||
R.string.sync_msg_storage_encryption_key_required,
|
||||
)
|
||||
AddStorageToSyncGroupResult.IncompatibleEncryption -> UserNotification.TextRes(
|
||||
R.string.sync_msg_storage_incompatible_encryption,
|
||||
)
|
||||
AddStorageToSyncGroupResult.GroupNotFound -> UserNotification.TextRes(
|
||||
R.string.sync_msg_group_removed,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,6 +239,7 @@ class StorageSyncViewModel @Inject constructor(
|
||||
val allStorages = vaultNodes
|
||||
.flatMap { (_, trees) -> trees.flatMap(::flattenStorages) }
|
||||
.distinctBy { it.uuid }
|
||||
storageByUuid = allStorages.associateBy { it.uuid }
|
||||
|
||||
if (allStorages.isEmpty()) {
|
||||
flowOf(
|
||||
@@ -245,7 +280,12 @@ class StorageSyncViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
.collect { mapped ->
|
||||
updateState(state.value.copy(vaults = mapped))
|
||||
updateState(
|
||||
state.value.copy(
|
||||
vaults = mapped,
|
||||
groups = reloadGroupsUi(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,7 +372,22 @@ class StorageSyncViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
private suspend fun reloadGroupsUi(): List<StorageSyncGroupUi> =
|
||||
groupsUseCase.getGroups().map { StorageSyncGroupUi(it.id, it.storageUuids) }
|
||||
groupsUseCase.getGroups().map { group ->
|
||||
val incompatible = group.storageUuids.filterTo(mutableSetOf()) { uuid ->
|
||||
val storage = storageByUuid[uuid] ?: return@filterTo true
|
||||
!isStorageCompatibleWithGroup(
|
||||
storage = storage,
|
||||
group = group,
|
||||
resolveStorageKey = vaultsManager.unlockManager::getOpenedStorageKey,
|
||||
)
|
||||
}
|
||||
StorageSyncGroupUi(
|
||||
id = group.id,
|
||||
storageUuids = group.storageUuids,
|
||||
encryptionKind = group.encryptionKind,
|
||||
incompatibleStorageUuids = incompatible,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun withGroupMutationBusy(
|
||||
clearPicker: Boolean = false,
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
<string name="sync_picker_collapse">Свернуть</string>
|
||||
<string name="sync_fab_create_group_cd">Создать группу синхронизации</string>
|
||||
<string name="sync_group_mixed_encryption_warning">В группе разное шифрование: задайте единый режим</string>
|
||||
<string name="sync_group_incompatible_warning">Несовместимые хранилища в группе: %1$d</string>
|
||||
<string name="sync_group_policy_line">Политика шифрования группы: %1$s</string>
|
||||
<string name="sync_group_policy_unset">Не определена (группа пуста)</string>
|
||||
<string name="sync_group_policy_plain">Только незашифрованные</string>
|
||||
<string name="sync_group_policy_password">Только зашифрованные с паролем группы</string>
|
||||
<string name="sync_remove_group_confirm_title">Удалить группу?</string>
|
||||
<string name="sync_remove_group_confirm_message">Удалить группу синхронизации «%1$s»?</string>
|
||||
<string name="sync_remove_storage_confirm_title">Убрать хранилище?</string>
|
||||
@@ -44,6 +49,10 @@
|
||||
<string name="sync_msg_group_removed">Группа удалена</string>
|
||||
<string name="sync_msg_storage_added">Хранилище добавлено в %1$s</string>
|
||||
<string name="sync_msg_storage_removed">Хранилище убрано из %1$s</string>
|
||||
<string name="sync_msg_storage_already_added">Хранилище уже добавлено в группу</string>
|
||||
<string name="sync_msg_storage_encryption_key_required">Для зашифрованного хранилища нужно знать пароль (откройте его перед добавлением)</string>
|
||||
<string name="sync_msg_storage_incompatible_encryption">Хранилище не совместимо с политикой шифрования группы</string>
|
||||
<string name="sync_msg_virtual_storage_not_supported">Нельзя добавлять открытое виртуальное хранилище: синхронизация работает с исходными raw storage</string>
|
||||
<string name="sync_msg_task_enqueued">Задача синхронизации поставлена в очередь</string>
|
||||
<string name="sync_msg_sync_already_running">Синхронизация уже выполняется</string>
|
||||
<string name="sync_msg_blocked_during_sync">Дождитесь окончания синхронизации</string>
|
||||
|
||||
Reference in New Issue
Block a user