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.unit.sp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.datatypes.Tree
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
import com.github.nullptroma.wallenc.ui.R import com.github.nullptroma.wallenc.ui.R
@@ -74,7 +75,10 @@ fun StorageTree(
val numOfFiles by cur.numberOfFiles.collectAsStateWithLifecycle() val numOfFiles by cur.numberOfFiles.collectAsStateWithLifecycle()
val size by cur.size.collectAsStateWithLifecycle() val size by cur.size.collectAsStateWithLifecycle()
val metaInfo by cur.metaInfo.collectAsStateWithLifecycle() val metaInfo by cur.metaInfo.collectAsStateWithLifecycle()
val metaLoadState by cur.metaLoadState.collectAsStateWithLifecycle()
val isAvailable by cur.isAvailable.collectAsStateWithLifecycle() val isAvailable by cur.isAvailable.collectAsStateWithLifecycle()
val metaUnavailable = metaLoadState == StorageMetaLoadState.Unavailable
val rowEnabled = isAvailable && !rowBusy && !metaUnavailable
val isEncrypted = metaInfo.encInfo != null val isEncrypted = metaInfo.encInfo != null
val isOpened = isEncryptionOpened(tree) val isOpened = isEncryptionOpened(tree)
val borderColor = val borderColor =
@@ -82,6 +86,7 @@ fun StorageTree(
val yesWord = stringResource(R.string.storage_value_yes) val yesWord = stringResource(R.string.storage_value_yes)
val noWord = stringResource(R.string.storage_value_no) val noWord = stringResource(R.string.storage_value_no)
val unavailableHint = stringResource(R.string.storage_unavailable_hint) val unavailableHint = stringResource(R.string.storage_unavailable_hint)
val metaUnavailableHint = stringResource(R.string.storage_meta_unavailable_hint)
Column(modifier) { Column(modifier) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -112,9 +117,9 @@ fun StorageTree(
elevation = CardDefaults.cardElevation( elevation = CardDefaults.cardElevation(
defaultElevation = 4.dp, defaultElevation = 4.dp,
), ),
enabled = isAvailable && !rowBusy, enabled = rowEnabled,
onClick = debouncedLambda(debounceMs = 500) { onClick = debouncedLambda(debounceMs = 500) {
if (isAvailable && !rowBusy) { if (rowEnabled) {
onClick(tree) onClick(tree)
} }
}, },
@@ -150,7 +155,13 @@ fun StorageTree(
), ),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
) )
if (!isAvailable) { if (metaUnavailable) {
Text(
text = metaUnavailableHint,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
} else if (!isAvailable) {
Text( Text(
text = unavailableHint, text = unavailableHint,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@@ -191,7 +202,7 @@ fun StorageTree(
} }
IconButton( IconButton(
onClick = { expanded = !expanded }, onClick = { expanded = !expanded },
enabled = isAvailable && !rowBusy, enabled = rowEnabled,
) { ) {
Icon( Icon(
Icons.Default.MoreVert, Icons.Default.MoreVert,
@@ -210,10 +221,10 @@ fun StorageTree(
onDismissRequest = { expanded = false }, onDismissRequest = { expanded = false },
) { ) {
DropdownMenuItem( DropdownMenuItem(
enabled = isAvailable && !rowBusy, enabled = rowEnabled,
onClick = { onClick = {
expanded = false expanded = false
if (isAvailable && !rowBusy) showRenameDialog = true if (rowEnabled) showRenameDialog = true
}, },
text = { text = {
Text( Text(
@@ -230,10 +241,10 @@ fun StorageTree(
) )
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
enabled = isAvailable && !rowBusy, enabled = rowEnabled,
onClick = { onClick = {
expanded = false expanded = false
if (isAvailable && !rowBusy) showRemoveConfirmDialog = true if (rowEnabled) showRemoveConfirmDialog = true
}, },
text = { text = {
Text( Text(
@@ -251,10 +262,10 @@ fun StorageTree(
if (!isEncrypted) { if (!isEncrypted) {
HorizontalDivider() HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
enabled = isAvailable && !rowBusy, enabled = rowEnabled,
onClick = { onClick = {
expanded = false expanded = false
if (isAvailable && !rowBusy) showSetupEncryptionDialog = true if (rowEnabled) showSetupEncryptionDialog = true
}, },
text = { text = {
Text( Text(
@@ -361,7 +372,7 @@ fun StorageTree(
if (isEncrypted) { if (isEncrypted) {
IconButton( IconButton(
onClick = { showLockDialog = true }, onClick = { showLockDialog = true },
enabled = isAvailable && !rowBusy, enabled = rowEnabled,
) { ) {
Icon( Icon(
if (isOpened) Icons.Default.LockOpen else Icons.Default.Lock, 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.SavedStateHandle
import androidx.lifecycle.viewModelScope 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.domain.errors.WallencException
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.ViewModelBase 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.ui.resources.toUserNotification
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
import com.github.nullptroma.wallenc.usecases.ManageTextSecretsUseCase import com.github.nullptroma.wallenc.usecases.ManageTextSecretsUseCase
@@ -43,11 +46,13 @@ class StorageHomeViewModel @Inject constructor(
combine( combine(
storage.isAvailable, storage.isAvailable,
storage.metaInfo, storage.metaInfo,
storage.metaLoadState,
manageTwoFaTokensUseCase.observe(storage), manageTwoFaTokensUseCase.observe(storage),
manageTextSecretsUseCase.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 isRawEncrypted = meta.encInfo != null && !storage.isVirtualStorage
val canManageDomainData = available && !isRawEncrypted val canManageDomainData = available && !isRawEncrypted && !metaUnavailable
state.value.copy( state.value.copy(
isLoading = false, isLoading = false,
storageUuid = storage.uuid.toString(), storageUuid = storage.uuid.toString(),
@@ -58,10 +63,10 @@ class StorageHomeViewModel @Inject constructor(
twoFaCount = twoFa.size, twoFaCount = twoFa.size,
textSecretsCount = secrets.size, textSecretsCount = secrets.size,
canManageDomainData = canManageDomainData, canManageDomainData = canManageDomainData,
errorNotification = if (isRawEncrypted) { errorNotification = when {
WallencException.Feature.NeedsDecryptedView().toUserNotification() metaUnavailable -> UserNotification.TextRes(R.string.storage_home_meta_unavailable)
} else { isRawEncrypted -> WallencException.Feature.NeedsDecryptedView().toUserNotification()
null else -> null
}, },
) )
}.collect { ui -> }.collect { ui ->

View File

@@ -3,6 +3,7 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.vault
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey 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.datatypes.Tree
import com.github.nullptroma.wallenc.domain.errors.toWallencException import com.github.nullptroma.wallenc.domain.errors.toWallencException
import com.github.nullptroma.wallenc.domain.interfaces.ILogger import com.github.nullptroma.wallenc.domain.interfaces.ILogger
@@ -253,9 +254,24 @@ abstract class AbstractVaultBrowserViewModel(
ManageStoragesEncryptionUseCase.CanEncryptResult.Allowed -> { ManageStoragesEncryptionUseCase.CanEncryptResult.Allowed -> {
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_encrypting)) ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_encrypting))
manageStoragesEncryptionUseCase.enableEncryption(storage, key, encryptPath) manageStoragesEncryptionUseCase.enableEncryption(storage, key, encryptPath)
manageStoragesEncryptionUseCase.openStorage(storage, key, rememberPassword) if (rememberPassword) {
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_encryption_enabled)) manageStoragesEncryptionUseCase.rememberStorageKey(storage, key)
_userNotifications.emit(UserNotification.TextRes(R.string.msg_encryption_enabled)) }
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 -> { ManageStoragesEncryptionUseCase.CanEncryptResult.AlreadyEncrypted -> {
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_already_encrypted)) ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_already_encrypted))
@@ -410,6 +426,9 @@ abstract class AbstractVaultBrowserViewModel(
@StringRes @StringRes
fun getStorageStatusRes(storage: IStorageInfo): Int { 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 val encrypted = storage.metaInfo.value.encInfo != null
if (!encrypted) return R.string.storage_status_not_encrypted if (!encrypted) return R.string.storage_status_not_encrypted
val opened = isEncryptionSessionOpen(storage) 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize 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 com.github.nullptroma.wallenc.ui.resources.UserNotification
import java.util.UUID import java.util.UUID
private val VaultRescanBottomInset = 88.dp
@Composable @Composable
fun VaultBrowserScreen( fun VaultBrowserScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -115,23 +114,35 @@ fun VaultBrowserScreen(
.fillMaxSize(), .fillMaxSize(),
) { ) {
uiState.header?.let { header -> uiState.header?.let { header ->
Column( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Column(modifier = Modifier.weight(1f)) {
text = stringResource(header.titleResId),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
)
header.subtitle?.let { subtitle ->
Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = subtitle, text = stringResource(header.titleResId),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, 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 { item {
Spacer( Spacer(modifier = Modifier.height(8.dp))
modifier = Modifier.height(
if (showRescan) VaultRescanBottomInset else 8.dp,
),
)
} }
} }
} }
@@ -220,18 +227,6 @@ fun VaultBrowserScreen(
content = vaultContent, 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) { if (showFullscreenLoader) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Box( Box(

View File

@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@@ -44,16 +45,24 @@ class StorageSyncViewModel @Inject constructor(
observeVaults() observeVaults()
observeStorageSyncPipeline() observeStorageSyncPipeline()
viewModelScope.launch { viewModelScope.launch {
vaultsManager.vaults combine(
.flatMapLatest { vaults -> vaultsManager.vaults,
if (vaults.isEmpty()) { state.map { it.groups },
flowOf(false) ) { vaults, groups ->
} else { val requiredUuids = groups.flatMap { it.storageUuids }.toSet()
combine(vaults.map { it.storagesScanInProgress }) { flags -> if (requiredUuids.isEmpty() || vaults.isEmpty()) {
flags.any { it } 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() .distinctUntilChanged()
.collect { scanning -> .collect { scanning ->
updateState(state.value.copy(anyVaultStoragesScanning = scanning)) updateState(state.value.copy(anyVaultStoragesScanning = scanning))

View File

@@ -17,7 +17,7 @@
<string name="screen_title_text_edit">Текст</string> <string name="screen_title_text_edit">Текст</string>
<string name="main_work_status_label">Статус:</string> <string name="main_work_status_label">Статус:</string>
<string name="main_status_multiple_tasks">Выполняется задач: %1$d</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="settings_title">Настройки</string>
<string name="sync_groups_title">Группы синхронизации</string> <string name="sync_groups_title">Группы синхронизации</string>
<string name="sync_progress_section_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_size">Размер: %1$s</string>
<string name="storage_field_virtual">Виртуальное: %1$s</string> <string name="storage_field_virtual">Виртуальное: %1$s</string>
<string name="storage_unavailable_hint">Хранилище недоступно</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_menu_unavailable">Недоступно: %1$s</string>
<string name="storage_status_not_encrypted">Не зашифровано</string> <string name="storage_status_not_encrypted">Не зашифровано</string>
<string name="storage_status_encrypted_open">Зашифровано (открыто)</string> <string name="storage_status_encrypted_open">Зашифровано (открыто)</string>
@@ -120,23 +123,23 @@
<string name="task_title_create_storage">Создание хранилища</string> <string name="task_title_create_storage">Создание хранилища</string>
<string name="task_title_enable_encryption">Включение шифрования</string> <string name="task_title_enable_encryption">Включение шифрования</string>
<string name="task_title_open_encrypted_storage">Расшифровка и открытие хранилища</string> <string name="task_title_open_encrypted_storage">Расшифровка и открытие хранилища</string>
<string name="task_progress_decrypt_running">Расшифровка</string> <string name="task_progress_decrypt_running">Расшифровка</string>
<string name="task_progress_dump_storage_log">Сканирование дерева</string> <string name="task_progress_dump_storage_log">Сканирование дерева</string>
<string name="task_progress_create_storage">Создание хранилища</string> <string name="task_progress_create_storage">Создание хранилища</string>
<string name="task_progress_enable_encryption">Шифрование</string> <string name="task_progress_enable_encryption">Шифрование</string>
<string name="task_progress_close_storage">Закрытие хранилища</string> <string name="task_progress_close_storage">Закрытие хранилища</string>
<string name="task_progress_disable_encryption">Очистка содержимого</string> <string name="task_progress_disable_encryption">Очистка содержимого</string>
<string name="task_progress_rename_storage">Переименование</string> <string name="task_progress_rename_storage">Переименование</string>
<string name="task_progress_remove_storage">Удаление</string> <string name="task_progress_remove_storage">Удаление</string>
<string name="task_progress_clear_sync_lock">Снятие блокировки</string> <string name="task_progress_clear_sync_lock">Снятие блокировки</string>
<string name="task_progress_add_remote_vault">Добавление</string> <string name="task_progress_add_remote_vault">Добавление</string>
<string name="task_progress_remove_remote_vault">Удаление</string> <string name="task_progress_remove_remote_vault">Удаление</string>
<string name="task_progress_retry_remote_vault">Подключение</string> <string name="task_progress_retry_remote_vault">Подключение</string>
<string name="task_progress_rescan_vault_storages">Сканирование хранилищ</string> <string name="task_progress_rescan_vault_storages">Сканирование хранилищ</string>
<string name="task_progress_save_2fa_token">Сохранение</string> <string name="task_progress_save_2fa_token">Сохранение</string>
<string name="task_progress_delete_2fa_token">Удаление</string> <string name="task_progress_delete_2fa_token">Удаление</string>
<string name="task_progress_save_text_secret">Сохранение</string> <string name="task_progress_save_text_secret">Сохранение</string>
<string name="task_progress_delete_text_secret">Удаление</string> <string name="task_progress_delete_text_secret">Удаление</string>
<string name="task_title_close_encrypted_storage">Закрытие зашифрованного хранилища</string> <string name="task_title_close_encrypted_storage">Закрытие зашифрованного хранилища</string>
<string name="task_title_disable_encryption">Отключение шифрования</string> <string name="task_title_disable_encryption">Отключение шифрования</string>
<string name="task_title_rename_storage">Переименование хранилища</string> <string name="task_title_rename_storage">Переименование хранилища</string>
@@ -175,6 +178,7 @@
<string name="vault_link_error_unknown">Не удалось войти</string> <string name="vault_link_error_unknown">Не удалось войти</string>
<string name="vault_link_error_unsupported_brand">Этот провайдер не поддерживается</string> <string name="vault_link_error_unsupported_brand">Этот провайдер не поддерживается</string>
<string name="msg_encryption_enabled">Шифрование включено</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_already_encrypted">Хранилище уже зашифровано</string>
<string name="msg_storage_not_empty">Хранилище не пустое</string> <string name="msg_storage_not_empty">Хранилище не пустое</string>
<string name="msg_storage_empty_state_unknown">Не удалось определить, пусто ли хранилище</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="screen_title_text_edit">Text</string>
<string name="main_work_status_label">Status:</string> <string name="main_work_status_label">Status:</string>
<string name="main_status_multiple_tasks">Running tasks: %1$d</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="settings_title">Settings</string>
<string name="sync_groups_title">Sync groups</string> <string name="sync_groups_title">Sync groups</string>
<string name="sync_progress_section_title">Storage sync</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_size">Size: %1$s</string>
<string name="storage_field_virtual">Virtual: %1$s</string> <string name="storage_field_virtual">Virtual: %1$s</string>
<string name="storage_unavailable_hint">Storage unavailable</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_menu_unavailable">Unavailable: %1$s</string>
<string name="storage_status_not_encrypted">Not encrypted</string> <string name="storage_status_not_encrypted">Not encrypted</string>
<string name="storage_status_encrypted_open">Encrypted (open)</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_create_storage">Create storage</string>
<string name="task_title_enable_encryption">Enable encryption</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_title_open_encrypted_storage">Decrypt and open storage</string>
<string name="task_progress_decrypt_running">Decrypting</string> <string name="task_progress_decrypt_running">Decrypting</string>
<string name="task_progress_dump_storage_log">Scanning tree</string> <string name="task_progress_dump_storage_log">Scanning tree</string>
<string name="task_progress_create_storage">Creating storage</string> <string name="task_progress_create_storage">Creating storage</string>
<string name="task_progress_enable_encryption">Encrypting</string> <string name="task_progress_enable_encryption">Encrypting</string>
<string name="task_progress_close_storage">Closing storage</string> <string name="task_progress_close_storage">Closing storage</string>
<string name="task_progress_disable_encryption">Clearing content</string> <string name="task_progress_disable_encryption">Clearing content</string>
<string name="task_progress_rename_storage">Renaming</string> <string name="task_progress_rename_storage">Renaming</string>
<string name="task_progress_remove_storage">Removing</string> <string name="task_progress_remove_storage">Removing</string>
<string name="task_progress_clear_sync_lock">Clearing sync lock</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_add_remote_vault">Adding</string>
<string name="task_progress_remove_remote_vault">Removing</string> <string name="task_progress_remove_remote_vault">Removing</string>
<string name="task_progress_retry_remote_vault">Connecting</string> <string name="task_progress_retry_remote_vault">Connecting</string>
<string name="task_progress_rescan_vault_storages">Scanning storages</string> <string name="task_progress_rescan_vault_storages">Scanning storages</string>
<string name="task_progress_save_2fa_token">Saving</string> <string name="task_progress_save_2fa_token">Saving</string>
<string name="task_progress_delete_2fa_token">Removing</string> <string name="task_progress_delete_2fa_token">Removing</string>
<string name="task_progress_save_text_secret">Saving</string> <string name="task_progress_save_text_secret">Saving</string>
<string name="task_progress_delete_text_secret">Removing</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_close_encrypted_storage">Close encrypted storage</string>
<string name="task_title_disable_encryption">Disable encryption</string> <string name="task_title_disable_encryption">Disable encryption</string>
<string name="task_title_rename_storage">Rename storage</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_unknown">Sign-in failed</string>
<string name="vault_link_error_unsupported_brand">This provider is not supported</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">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_already_encrypted">Storage is already encrypted</string>
<string name="msg_storage_not_empty">Storage is not empty</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> <string name="msg_storage_empty_state_unknown">Could not determine if storage is empty</string>