From 671f1f1c2ade3bc24459d7335e6a500d8d2209be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=8B=D1=82=D0=BA=D0=BE=D0=B2=20=D0=A0=D0=BE=D0=BC?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Thu, 21 May 2026 11:05:25 +0300 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B8?= =?UTF-8?q?=D0=BB=20vault/sync=20UX=20=D0=B8=20=D0=BF=D0=BE=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rescan в заголовке vault, sync-кнопка только при скане релевантных vault, блокировка UI при недоступных meta, remember/open после encrypt, убрал … из task_progress (точки остаются в foreground-сервисе). --- .../wallenc/ui/elements/StorageTree.kt | 33 +++++++---- .../screens/storage/StorageHomeViewModel.kt | 17 ++++-- .../vault/AbstractVaultBrowserViewModel.kt | 25 ++++++++- .../main/screens/vault/VaultBrowserScreen.kt | 55 +++++++++---------- .../ui/screens/sync/StorageSyncViewModel.kt | 25 ++++++--- ui/src/main/res/values-ru/strings.xml | 40 ++++++++------ ui/src/main/res/values/strings.xml | 40 ++++++++------ 7 files changed, 141 insertions(+), 94 deletions(-) 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