diff --git a/ui/src/main/java/com/github/nullptroma/wallenc/ui/elements/StorageTree.kt b/ui/src/main/java/com/github/nullptroma/wallenc/ui/elements/StorageTree.kt
index 639022b..95c42c5 100644
--- a/ui/src/main/java/com/github/nullptroma/wallenc/ui/elements/StorageTree.kt
+++ b/ui/src/main/java/com/github/nullptroma/wallenc/ui/elements/StorageTree.kt
@@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.github.nullptroma.wallenc.domain.datatypes.StorageMetaLoadState
import com.github.nullptroma.wallenc.domain.datatypes.Tree
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
import com.github.nullptroma.wallenc.ui.R
@@ -74,7 +75,10 @@ fun StorageTree(
val numOfFiles by cur.numberOfFiles.collectAsStateWithLifecycle()
val size by cur.size.collectAsStateWithLifecycle()
val metaInfo by cur.metaInfo.collectAsStateWithLifecycle()
+ val metaLoadState by cur.metaLoadState.collectAsStateWithLifecycle()
val isAvailable by cur.isAvailable.collectAsStateWithLifecycle()
+ val metaUnavailable = metaLoadState == StorageMetaLoadState.Unavailable
+ val rowEnabled = isAvailable && !rowBusy && !metaUnavailable
val isEncrypted = metaInfo.encInfo != null
val isOpened = isEncryptionOpened(tree)
val borderColor =
@@ -82,6 +86,7 @@ fun StorageTree(
val yesWord = stringResource(R.string.storage_value_yes)
val noWord = stringResource(R.string.storage_value_no)
val unavailableHint = stringResource(R.string.storage_unavailable_hint)
+ val metaUnavailableHint = stringResource(R.string.storage_meta_unavailable_hint)
Column(modifier) {
Box(
modifier = Modifier
@@ -112,9 +117,9 @@ fun StorageTree(
elevation = CardDefaults.cardElevation(
defaultElevation = 4.dp,
),
- enabled = isAvailable && !rowBusy,
+ enabled = rowEnabled,
onClick = debouncedLambda(debounceMs = 500) {
- if (isAvailable && !rowBusy) {
+ if (rowEnabled) {
onClick(tree)
}
},
@@ -150,7 +155,13 @@ fun StorageTree(
),
style = MaterialTheme.typography.bodySmall,
)
- if (!isAvailable) {
+ if (metaUnavailable) {
+ Text(
+ text = metaUnavailableHint,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.error,
+ )
+ } else if (!isAvailable) {
Text(
text = unavailableHint,
style = MaterialTheme.typography.bodySmall,
@@ -191,7 +202,7 @@ fun StorageTree(
}
IconButton(
onClick = { expanded = !expanded },
- enabled = isAvailable && !rowBusy,
+ enabled = rowEnabled,
) {
Icon(
Icons.Default.MoreVert,
@@ -210,10 +221,10 @@ fun StorageTree(
onDismissRequest = { expanded = false },
) {
DropdownMenuItem(
- enabled = isAvailable && !rowBusy,
+ enabled = rowEnabled,
onClick = {
expanded = false
- if (isAvailable && !rowBusy) showRenameDialog = true
+ if (rowEnabled) showRenameDialog = true
},
text = {
Text(
@@ -230,10 +241,10 @@ fun StorageTree(
)
HorizontalDivider()
DropdownMenuItem(
- enabled = isAvailable && !rowBusy,
+ enabled = rowEnabled,
onClick = {
expanded = false
- if (isAvailable && !rowBusy) showRemoveConfirmDialog = true
+ if (rowEnabled) showRemoveConfirmDialog = true
},
text = {
Text(
@@ -251,10 +262,10 @@ fun StorageTree(
if (!isEncrypted) {
HorizontalDivider()
DropdownMenuItem(
- enabled = isAvailable && !rowBusy,
+ enabled = rowEnabled,
onClick = {
expanded = false
- if (isAvailable && !rowBusy) showSetupEncryptionDialog = true
+ if (rowEnabled) showSetupEncryptionDialog = true
},
text = {
Text(
@@ -361,7 +372,7 @@ fun StorageTree(
if (isEncrypted) {
IconButton(
onClick = { showLockDialog = true },
- enabled = isAvailable && !rowBusy,
+ enabled = rowEnabled,
) {
Icon(
if (isOpened) Icons.Default.LockOpen else Icons.Default.Lock,
diff --git a/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/storage/StorageHomeViewModel.kt b/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/storage/StorageHomeViewModel.kt
index 707da72..5155948 100644
--- a/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/storage/StorageHomeViewModel.kt
+++ b/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/storage/StorageHomeViewModel.kt
@@ -2,8 +2,11 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.storage
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
+import com.github.nullptroma.wallenc.domain.datatypes.StorageMetaLoadState
import com.github.nullptroma.wallenc.domain.errors.WallencException
+import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.ViewModelBase
+import com.github.nullptroma.wallenc.ui.resources.UserNotification
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
import com.github.nullptroma.wallenc.usecases.ManageTextSecretsUseCase
@@ -43,11 +46,13 @@ class StorageHomeViewModel @Inject constructor(
combine(
storage.isAvailable,
storage.metaInfo,
+ storage.metaLoadState,
manageTwoFaTokensUseCase.observe(storage),
manageTextSecretsUseCase.observe(storage),
- ) { available, meta, twoFa, secrets ->
+ ) { available, meta, metaState, twoFa, secrets ->
+ val metaUnavailable = metaState == StorageMetaLoadState.Unavailable
val isRawEncrypted = meta.encInfo != null && !storage.isVirtualStorage
- val canManageDomainData = available && !isRawEncrypted
+ val canManageDomainData = available && !isRawEncrypted && !metaUnavailable
state.value.copy(
isLoading = false,
storageUuid = storage.uuid.toString(),
@@ -58,10 +63,10 @@ class StorageHomeViewModel @Inject constructor(
twoFaCount = twoFa.size,
textSecretsCount = secrets.size,
canManageDomainData = canManageDomainData,
- errorNotification = if (isRawEncrypted) {
- WallencException.Feature.NeedsDecryptedView().toUserNotification()
- } else {
- null
+ errorNotification = when {
+ metaUnavailable -> UserNotification.TextRes(R.string.storage_home_meta_unavailable)
+ isRawEncrypted -> WallencException.Feature.NeedsDecryptedView().toUserNotification()
+ else -> null
},
)
}.collect { ui ->
diff --git a/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/vault/AbstractVaultBrowserViewModel.kt b/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/vault/AbstractVaultBrowserViewModel.kt
index 1c978ed..a45b7e1 100644
--- a/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/vault/AbstractVaultBrowserViewModel.kt
+++ b/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/vault/AbstractVaultBrowserViewModel.kt
@@ -3,6 +3,7 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.vault
import androidx.annotation.StringRes
import androidx.lifecycle.viewModelScope
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
+import com.github.nullptroma.wallenc.domain.datatypes.StorageMetaLoadState
import com.github.nullptroma.wallenc.domain.datatypes.Tree
import com.github.nullptroma.wallenc.domain.errors.toWallencException
import com.github.nullptroma.wallenc.domain.interfaces.ILogger
@@ -253,9 +254,24 @@ abstract class AbstractVaultBrowserViewModel(
ManageStoragesEncryptionUseCase.CanEncryptResult.Allowed -> {
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_encrypting))
manageStoragesEncryptionUseCase.enableEncryption(storage, key, encryptPath)
- manageStoragesEncryptionUseCase.openStorage(storage, key, rememberPassword)
- ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_encryption_enabled))
- _userNotifications.emit(UserNotification.TextRes(R.string.msg_encryption_enabled))
+ if (rememberPassword) {
+ manageStoragesEncryptionUseCase.rememberStorageKey(storage, key)
+ }
+ try {
+ manageStoragesEncryptionUseCase.openStorage(
+ storage,
+ key,
+ rememberPassword = false,
+ )
+ ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_encryption_enabled))
+ _userNotifications.emit(UserNotification.TextRes(R.string.msg_encryption_enabled))
+ } catch (openError: Exception) {
+ logger.debug(TAG, "open after encrypt failed: ${openError.stackTraceToString()}")
+ ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_encryption_enabled))
+ _userNotifications.emit(
+ UserNotification.TextRes(R.string.msg_encryption_enabled_open_failed),
+ )
+ }
}
ManageStoragesEncryptionUseCase.CanEncryptResult.AlreadyEncrypted -> {
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_already_encrypted))
@@ -410,6 +426,9 @@ abstract class AbstractVaultBrowserViewModel(
@StringRes
fun getStorageStatusRes(storage: IStorageInfo): Int {
+ if (storage.metaLoadState.value == StorageMetaLoadState.Unavailable) {
+ return R.string.storage_status_meta_unavailable
+ }
val encrypted = storage.metaInfo.value.encInfo != null
if (!encrypted) return R.string.storage_status_not_encrypted
val opened = isEncryptionSessionOpen(storage)
diff --git a/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/vault/VaultBrowserScreen.kt b/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/vault/VaultBrowserScreen.kt
index b49e9aa..03e60b5 100644
--- a/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/vault/VaultBrowserScreen.kt
+++ b/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/main/screens/vault/VaultBrowserScreen.kt
@@ -5,6 +5,7 @@ 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.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
@@ -42,8 +43,6 @@ import com.github.nullptroma.wallenc.ui.elements.StorageTree
import com.github.nullptroma.wallenc.ui.resources.UserNotification
import java.util.UUID
-private val VaultRescanBottomInset = 88.dp
-
@Composable
fun VaultBrowserScreen(
modifier: Modifier = Modifier,
@@ -115,23 +114,35 @@ fun VaultBrowserScreen(
.fillMaxSize(),
) {
uiState.header?.let { header ->
- Column(
+ Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
) {
- Text(
- text = stringResource(header.titleResId),
- style = MaterialTheme.typography.headlineSmall,
- color = MaterialTheme.colorScheme.onSurface,
- )
- header.subtitle?.let { subtitle ->
- Spacer(modifier = Modifier.height(4.dp))
+ Column(modifier = Modifier.weight(1f)) {
Text(
- text = subtitle,
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
+ text = stringResource(header.titleResId),
+ style = MaterialTheme.typography.headlineSmall,
+ color = MaterialTheme.colorScheme.onSurface,
)
+ header.subtitle?.let { subtitle ->
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+ if (showRescan) {
+ FilledTonalButton(
+ onClick = { viewModel.rescanStorages() },
+ enabled = rescanEnabled,
+ ) {
+ Text(stringResource(R.string.vault_rescan_storages_action))
+ }
}
}
}
@@ -200,11 +211,7 @@ fun VaultBrowserScreen(
)
}
item {
- Spacer(
- modifier = Modifier.height(
- if (showRescan) VaultRescanBottomInset else 8.dp,
- ),
- )
+ Spacer(modifier = Modifier.height(8.dp))
}
}
}
@@ -220,18 +227,6 @@ fun VaultBrowserScreen(
content = vaultContent,
)
- if (showRescan) {
- FilledTonalButton(
- onClick = { viewModel.rescanStorages() },
- enabled = rescanEnabled,
- modifier = Modifier
- .align(Alignment.BottomCenter)
- .padding(bottom = VaultRescanBottomInset),
- ) {
- Text(stringResource(R.string.vault_rescan_storages_action))
- }
- }
-
if (showFullscreenLoader) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Box(
diff --git a/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/sync/StorageSyncViewModel.kt b/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/sync/StorageSyncViewModel.kt
index 12d5412..901c55b 100644
--- a/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/sync/StorageSyncViewModel.kt
+++ b/ui/src/main/java/com/github/nullptroma/wallenc/ui/screens/sync/StorageSyncViewModel.kt
@@ -24,6 +24,7 @@ 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
import kotlinx.coroutines.launch
import java.util.UUID
import javax.inject.Inject
@@ -44,16 +45,24 @@ class StorageSyncViewModel @Inject constructor(
observeVaults()
observeStorageSyncPipeline()
viewModelScope.launch {
- vaultsManager.vaults
- .flatMapLatest { vaults ->
- if (vaults.isEmpty()) {
- flowOf(false)
- } else {
- combine(vaults.map { it.storagesScanInProgress }) { flags ->
- flags.any { it }
- }
+ combine(
+ vaultsManager.vaults,
+ state.map { it.groups },
+ ) { vaults, groups ->
+ val requiredUuids = groups.flatMap { it.storageUuids }.toSet()
+ if (requiredUuids.isEmpty() || vaults.isEmpty()) {
+ false
+ } else {
+ val opened = vaultsManager.unlockManager.openedStorages.value
+ vaults.any { vault ->
+ val uuidsInVault = vault.storages.value.flatMap { root ->
+ flattenStorages(buildStorageTree(root, opened))
+ }.map { it.uuid }.toSet()
+ uuidsInVault.any { it in requiredUuids } &&
+ vault.storagesScanInProgress.value
}
}
+ }
.distinctUntilChanged()
.collect { scanning ->
updateState(state.value.copy(anyVaultStoragesScanning = scanning))
diff --git a/ui/src/main/res/values-ru/strings.xml b/ui/src/main/res/values-ru/strings.xml
index 8904a53..24c851b 100644
--- a/ui/src/main/res/values-ru/strings.xml
+++ b/ui/src/main/res/values-ru/strings.xml
@@ -17,7 +17,7 @@
Текст
Статус:
Выполняется задач: %1$d
- Сканирование vault: загрузка списка хранилищ…
+ Сканирование vault: загрузка списка хранилищ
Настройки
Группы синхронизации
Синхронизация хранилищ
@@ -83,6 +83,9 @@
Размер: %1$s
Виртуальное: %1$s
Хранилище недоступно
+ Метаданные недоступны — переименование, шифрование и открытие отключены
+ Метаданные недоступны
+ Не удалось загрузить метаданные хранилища. 2FA и текстовые секреты недоступны.
Недоступно: %1$s
Не зашифровано
Зашифровано (открыто)
@@ -120,23 +123,23 @@
Создание хранилища
Включение шифрования
Расшифровка и открытие хранилища
- Расшифровка…
- Сканирование дерева…
- Создание хранилища…
- Шифрование…
- Закрытие хранилища…
- Очистка содержимого…
- Переименование…
- Удаление…
- Снятие блокировки…
- Добавление…
- Удаление…
- Подключение…
- Сканирование хранилищ…
- Сохранение…
- Удаление…
- Сохранение…
- Удаление…
+ Расшифровка
+ Сканирование дерева
+ Создание хранилища
+ Шифрование
+ Закрытие хранилища
+ Очистка содержимого
+ Переименование
+ Удаление
+ Снятие блокировки
+ Добавление
+ Удаление
+ Подключение
+ Сканирование хранилищ
+ Сохранение
+ Удаление
+ Сохранение
+ Удаление
Закрытие зашифрованного хранилища
Отключение шифрования
Переименование хранилища
@@ -175,6 +178,7 @@
Не удалось войти
Этот провайдер не поддерживается
Шифрование включено
+ Шифрование включено; откройте хранилище вручную для просмотра
Хранилище уже зашифровано
Хранилище не пустое
Не удалось определить, пусто ли хранилище
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index 0a2a83b..fa35c6d 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -17,7 +17,7 @@
Text
Status:
Running tasks: %1$d
- Scanning vault: loading storage list…
+ Scanning vault: loading storage list
Settings
Sync groups
Storage sync
@@ -83,6 +83,9 @@
Size: %1$s
Virtual: %1$s
Storage unavailable
+ Metadata unavailable — rename, encryption, and open are disabled
+ Metadata unavailable
+ Storage metadata could not be loaded. 2FA and text secrets are unavailable.
Unavailable: %1$s
Not encrypted
Encrypted (open)
@@ -120,23 +123,23 @@
Create storage
Enable encryption
Decrypt and open storage
- Decrypting…
- Scanning tree…
- Creating storage…
- Encrypting…
- Closing storage…
- Clearing content…
- Renaming…
- Removing…
- Clearing sync lock…
- Adding…
- Removing…
- Connecting…
- Scanning storages…
- Saving…
- Removing…
- Saving…
- Removing…
+ Decrypting
+ Scanning tree
+ Creating storage
+ Encrypting
+ Closing storage
+ Clearing content
+ Renaming
+ Removing
+ Clearing sync lock
+ Adding
+ Removing
+ Connecting
+ Scanning storages
+ Saving
+ Removing
+ Saving
+ Removing
Close encrypted storage
Disable encryption
Rename storage
@@ -175,6 +178,7 @@
Sign-in failed
This provider is not supported
Encryption enabled
+ Encryption enabled; unlock the storage manually to view contents
Storage is already encrypted
Storage is not empty
Could not determine if storage is empty