fix(ui): улучшил vault/sync UX и подписи прогресса

Rescan в заголовке vault, sync-кнопка только при скане релевантных vault,
блокировка UI при недоступных meta, remember/open после encrypt,
убрал … из task_progress (точки остаются в foreground-сервисе).
This commit is contained in:
2026-05-21 11:05:25 +03:00
parent 467ed64426
commit 671f1f1c2a
7 changed files with 141 additions and 94 deletions

View File

@@ -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,

View File

@@ -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 ->

View File

@@ -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)
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)

View File

@@ -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,11 +114,14 @@ 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,
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(header.titleResId),
style = MaterialTheme.typography.headlineSmall,
@@ -134,6 +136,15 @@ fun VaultBrowserScreen(
)
}
}
if (showRescan) {
FilledTonalButton(
onClick = { viewModel.rescanStorages() },
enabled = rescanEnabled,
) {
Text(stringResource(R.string.vault_rescan_storages_action))
}
}
}
}
if (!fabEnabled) {
Text(
@@ -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(

View File

@@ -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,13 +45,21 @@ class StorageSyncViewModel @Inject constructor(
observeVaults()
observeStorageSyncPipeline()
viewModelScope.launch {
vaultsManager.vaults
.flatMapLatest { vaults ->
if (vaults.isEmpty()) {
flowOf(false)
combine(
vaultsManager.vaults,
state.map { it.groups },
) { vaults, groups ->
val requiredUuids = groups.flatMap { it.storageUuids }.toSet()
if (requiredUuids.isEmpty() || vaults.isEmpty()) {
false
} else {
combine(vaults.map { it.storagesScanInProgress }) { flags ->
flags.any { it }
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
}
}
}

View File

@@ -17,7 +17,7 @@
<string name="screen_title_text_edit">Текст</string>
<string name="main_work_status_label">Статус:</string>
<string name="main_status_multiple_tasks">Выполняется задач: %1$d</string>
<string name="main_status_vault_scanning_storages">Сканирование vault: загрузка списка хранилищ</string>
<string name="main_status_vault_scanning_storages">Сканирование vault: загрузка списка хранилищ</string>
<string name="settings_title">Настройки</string>
<string name="sync_groups_title">Группы синхронизации</string>
<string name="sync_progress_section_title">Синхронизация хранилищ</string>
@@ -83,6 +83,9 @@
<string name="storage_field_size">Размер: %1$s</string>
<string name="storage_field_virtual">Виртуальное: %1$s</string>
<string name="storage_unavailable_hint">Хранилище недоступно</string>
<string name="storage_meta_unavailable_hint">Метаданные недоступны — переименование, шифрование и открытие отключены</string>
<string name="storage_status_meta_unavailable">Метаданные недоступны</string>
<string name="storage_home_meta_unavailable">Не удалось загрузить метаданные хранилища. 2FA и текстовые секреты недоступны.</string>
<string name="storage_menu_unavailable">Недоступно: %1$s</string>
<string name="storage_status_not_encrypted">Не зашифровано</string>
<string name="storage_status_encrypted_open">Зашифровано (открыто)</string>
@@ -120,23 +123,23 @@
<string name="task_title_create_storage">Создание хранилища</string>
<string name="task_title_enable_encryption">Включение шифрования</string>
<string name="task_title_open_encrypted_storage">Расшифровка и открытие хранилища</string>
<string name="task_progress_decrypt_running">Расшифровка</string>
<string name="task_progress_dump_storage_log">Сканирование дерева</string>
<string name="task_progress_create_storage">Создание хранилища</string>
<string name="task_progress_enable_encryption">Шифрование</string>
<string name="task_progress_close_storage">Закрытие хранилища</string>
<string name="task_progress_disable_encryption">Очистка содержимого</string>
<string name="task_progress_rename_storage">Переименование</string>
<string name="task_progress_remove_storage">Удаление</string>
<string name="task_progress_clear_sync_lock">Снятие блокировки</string>
<string name="task_progress_add_remote_vault">Добавление</string>
<string name="task_progress_remove_remote_vault">Удаление</string>
<string name="task_progress_retry_remote_vault">Подключение</string>
<string name="task_progress_rescan_vault_storages">Сканирование хранилищ</string>
<string name="task_progress_save_2fa_token">Сохранение</string>
<string name="task_progress_delete_2fa_token">Удаление</string>
<string name="task_progress_save_text_secret">Сохранение</string>
<string name="task_progress_delete_text_secret">Удаление</string>
<string name="task_progress_decrypt_running">Расшифровка</string>
<string name="task_progress_dump_storage_log">Сканирование дерева</string>
<string name="task_progress_create_storage">Создание хранилища</string>
<string name="task_progress_enable_encryption">Шифрование</string>
<string name="task_progress_close_storage">Закрытие хранилища</string>
<string name="task_progress_disable_encryption">Очистка содержимого</string>
<string name="task_progress_rename_storage">Переименование</string>
<string name="task_progress_remove_storage">Удаление</string>
<string name="task_progress_clear_sync_lock">Снятие блокировки</string>
<string name="task_progress_add_remote_vault">Добавление</string>
<string name="task_progress_remove_remote_vault">Удаление</string>
<string name="task_progress_retry_remote_vault">Подключение</string>
<string name="task_progress_rescan_vault_storages">Сканирование хранилищ</string>
<string name="task_progress_save_2fa_token">Сохранение</string>
<string name="task_progress_delete_2fa_token">Удаление</string>
<string name="task_progress_save_text_secret">Сохранение</string>
<string name="task_progress_delete_text_secret">Удаление</string>
<string name="task_title_close_encrypted_storage">Закрытие зашифрованного хранилища</string>
<string name="task_title_disable_encryption">Отключение шифрования</string>
<string name="task_title_rename_storage">Переименование хранилища</string>
@@ -175,6 +178,7 @@
<string name="vault_link_error_unknown">Не удалось войти</string>
<string name="vault_link_error_unsupported_brand">Этот провайдер не поддерживается</string>
<string name="msg_encryption_enabled">Шифрование включено</string>
<string name="msg_encryption_enabled_open_failed">Шифрование включено; откройте хранилище вручную для просмотра</string>
<string name="msg_storage_already_encrypted">Хранилище уже зашифровано</string>
<string name="msg_storage_not_empty">Хранилище не пустое</string>
<string name="msg_storage_empty_state_unknown">Не удалось определить, пусто ли хранилище</string>

View File

@@ -17,7 +17,7 @@
<string name="screen_title_text_edit">Text</string>
<string name="main_work_status_label">Status:</string>
<string name="main_status_multiple_tasks">Running tasks: %1$d</string>
<string name="main_status_vault_scanning_storages">Scanning vault: loading storage list</string>
<string name="main_status_vault_scanning_storages">Scanning vault: loading storage list</string>
<string name="settings_title">Settings</string>
<string name="sync_groups_title">Sync groups</string>
<string name="sync_progress_section_title">Storage sync</string>
@@ -83,6 +83,9 @@
<string name="storage_field_size">Size: %1$s</string>
<string name="storage_field_virtual">Virtual: %1$s</string>
<string name="storage_unavailable_hint">Storage unavailable</string>
<string name="storage_meta_unavailable_hint">Metadata unavailable — rename, encryption, and open are disabled</string>
<string name="storage_status_meta_unavailable">Metadata unavailable</string>
<string name="storage_home_meta_unavailable">Storage metadata could not be loaded. 2FA and text secrets are unavailable.</string>
<string name="storage_menu_unavailable">Unavailable: %1$s</string>
<string name="storage_status_not_encrypted">Not encrypted</string>
<string name="storage_status_encrypted_open">Encrypted (open)</string>
@@ -120,23 +123,23 @@
<string name="task_title_create_storage">Create storage</string>
<string name="task_title_enable_encryption">Enable encryption</string>
<string name="task_title_open_encrypted_storage">Decrypt and open storage</string>
<string name="task_progress_decrypt_running">Decrypting</string>
<string name="task_progress_dump_storage_log">Scanning tree</string>
<string name="task_progress_create_storage">Creating storage</string>
<string name="task_progress_enable_encryption">Encrypting</string>
<string name="task_progress_close_storage">Closing storage</string>
<string name="task_progress_disable_encryption">Clearing content</string>
<string name="task_progress_rename_storage">Renaming</string>
<string name="task_progress_remove_storage">Removing</string>
<string name="task_progress_clear_sync_lock">Clearing sync lock</string>
<string name="task_progress_add_remote_vault">Adding</string>
<string name="task_progress_remove_remote_vault">Removing</string>
<string name="task_progress_retry_remote_vault">Connecting</string>
<string name="task_progress_rescan_vault_storages">Scanning storages</string>
<string name="task_progress_save_2fa_token">Saving</string>
<string name="task_progress_delete_2fa_token">Removing</string>
<string name="task_progress_save_text_secret">Saving</string>
<string name="task_progress_delete_text_secret">Removing</string>
<string name="task_progress_decrypt_running">Decrypting</string>
<string name="task_progress_dump_storage_log">Scanning tree</string>
<string name="task_progress_create_storage">Creating storage</string>
<string name="task_progress_enable_encryption">Encrypting</string>
<string name="task_progress_close_storage">Closing storage</string>
<string name="task_progress_disable_encryption">Clearing content</string>
<string name="task_progress_rename_storage">Renaming</string>
<string name="task_progress_remove_storage">Removing</string>
<string name="task_progress_clear_sync_lock">Clearing sync lock</string>
<string name="task_progress_add_remote_vault">Adding</string>
<string name="task_progress_remove_remote_vault">Removing</string>
<string name="task_progress_retry_remote_vault">Connecting</string>
<string name="task_progress_rescan_vault_storages">Scanning storages</string>
<string name="task_progress_save_2fa_token">Saving</string>
<string name="task_progress_delete_2fa_token">Removing</string>
<string name="task_progress_save_text_secret">Saving</string>
<string name="task_progress_delete_text_secret">Removing</string>
<string name="task_title_close_encrypted_storage">Close encrypted storage</string>
<string name="task_title_disable_encryption">Disable encryption</string>
<string name="task_title_rename_storage">Rename storage</string>
@@ -175,6 +178,7 @@
<string name="vault_link_error_unknown">Sign-in failed</string>
<string name="vault_link_error_unsupported_brand">This provider is not supported</string>
<string name="msg_encryption_enabled">Encryption enabled</string>
<string name="msg_encryption_enabled_open_failed">Encryption enabled; unlock the storage manually to view contents</string>
<string name="msg_storage_already_encrypted">Storage is already encrypted</string>
<string name="msg_storage_not_empty">Storage is not empty</string>
<string name="msg_storage_empty_state_unknown">Could not determine if storage is empty</string>