Сильно улучшен UX при работе с Yandex vault

This commit is contained in:
2026-05-21 01:40:30 +03:00
parent 9c38da76d2
commit c58bcdc35b
19 changed files with 350 additions and 118 deletions

View File

@@ -58,6 +58,7 @@ fun TaskProgressLabel.resolve(resolver: UiStringResolver): String = when (this)
VaultTaskStep.AddRemoteVault -> resolver(R.string.task_progress_add_remote_vault)
VaultTaskStep.RemoveRemoteVault -> resolver(R.string.task_progress_remove_remote_vault)
VaultTaskStep.RetryRemoteVault -> resolver(R.string.task_progress_retry_remote_vault)
VaultTaskStep.RescanVaultStorages -> resolver(R.string.task_progress_rescan_vault_storages)
VaultTaskStep.Save2FaToken -> resolver(R.string.task_progress_save_2fa_token)
VaultTaskStep.Delete2FaToken -> resolver(R.string.task_progress_delete_2fa_token)
VaultTaskStep.SaveTextSecret -> resolver(R.string.task_progress_save_text_secret)

View File

@@ -1,6 +1,7 @@
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage
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.size
@@ -18,6 +19,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
@@ -45,7 +47,12 @@ fun StorageHomeScreen(
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
if (uiState.isLoading) {
CircularProgressIndicator()
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
return@Column
}

View File

@@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import java.util.UUID
@@ -39,6 +40,8 @@ import java.util.UUID
*/
abstract class AbstractVaultBrowserViewModel(
storagesFlow: Flow<List<IStorage>>,
private val storagesScanInProgressFlow: Flow<Boolean> = flowOf(false),
private val vaultHeaderFlow: Flow<VaultBrowserHeader?> = flowOf(null),
private val vaultAvailabilityFlow: Flow<Boolean>,
private val resolveCreateVaultUuid: () -> UUID?,
private val removeStorageUseCase: RemoveStorageUseCase,
@@ -63,8 +66,12 @@ abstract class AbstractVaultBrowserViewModel(
private val _userNotifications = MutableSharedFlow<UserNotification>(extraBufferCapacity = 8)
val userNotifications: SharedFlow<UserNotification> = _userNotifications
/** Удалённый vault: показать кнопку повторного сканирования storages на Диске. */
open val supportsStorageRescan: Boolean = false
init {
collectStoragesFlow(storagesFlow)
collectVaultHeaderFlow()
collectPipelineBusyFlags()
viewModelScope.launch {
vaultAvailabilityFlow
@@ -94,31 +101,41 @@ abstract class AbstractVaultBrowserViewModel(
t.locksVaultStorageList && isPipelineTaskActive(t.state)
}
private fun collectVaultHeaderFlow() {
viewModelScope.launch {
vaultHeaderFlow.collect { header ->
updateState(state.value.copy(header = header))
}
}
}
private fun collectStoragesFlow(storagesFlow: Flow<List<IStorage>>) {
viewModelScope.launch {
combine(
storagesFlow,
storagesScanInProgressFlow,
getOpenedStoragesUseCase.openedStorages,
) { storages, opened -> storages to opened }
.collect { (storages, opened) ->
val list = mutableListOf<Tree<IStorageInfo>>()
for (storage in storages) {
var tree = Tree<IStorageInfo>(storage)
list.add(tree)
while (opened.containsKey(tree.value.uuid)) {
val child = opened.getValue(tree.value.uuid)
val nextTree = Tree(child)
tree.children = listOf(nextTree)
tree = nextTree
}
) { storages, scanInProgress, opened ->
Triple(storages, scanInProgress, opened)
}.collect { (storages, scanInProgress, opened) ->
val list = mutableListOf<Tree<IStorageInfo>>()
for (storage in storages) {
var tree = Tree<IStorageInfo>(storage)
list.add(tree)
while (opened.containsKey(tree.value.uuid)) {
val child = opened.getValue(tree.value.uuid)
val nextTree = Tree(child)
tree.children = listOf(nextTree)
tree = nextTree
}
updateState(
state.value.copy(
storagesList = list,
storagesRefreshing = false,
),
)
}
updateState(
state.value.copy(
storagesList = list,
storagesRefreshing = scanInProgress,
),
)
}
}
}
@@ -149,6 +166,35 @@ abstract class AbstractVaultBrowserViewModel(
}
}
fun rescanStorages() {
if (!supportsStorageRescan) return
if (state.value.storagesRefreshing) {
notifyUser(R.string.vault_msg_rescan_already_in_progress)
return
}
if (isVaultListMutationActive()) {
notifyUser(R.string.vault_msg_vault_list_mutation_busy)
return
}
val vaultUuid = resolveCreateVaultUuid() ?: return
taskOrchestrator.enqueue(
title = uiStrings(R.string.task_title_rescan_vault_storages),
dispatcher = Dispatchers.IO,
locksVaultStorageList = true,
work = { ctx ->
try {
ctx.reportProgress(null, TaskProgressLabel.VaultTask(VaultTaskStep.RescanVaultStorages))
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_rescanning_vault_storages))
manageVaultUseCase.rescanStorages(vaultUuid)
ctx.log(TaskLogLevel.Info, uiStrings(R.string.task_log_rescan_vault_storages_done))
} catch (e: Exception) {
logger.debug(TAG, "rescanStorages failed: ${e.stackTraceToString()}")
ctx.log(TaskLogLevel.Error, uiStrings(R.string.task_log_rescan_vault_storages_failed))
}
},
)
}
fun createStorage() {
if (!state.value.addStorageFabEnabled) {
logger.debug(TAG, "createStorage ignored (vault unavailable or FAB disabled)")

View File

@@ -1,7 +1,9 @@
package com.github.nullptroma.wallenc.ui.screens.main.screens.vault
import com.github.nullptroma.wallenc.domain.interfaces.ILogger
import com.github.nullptroma.wallenc.domain.interfaces.IVault
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
import com.github.nullptroma.wallenc.usecases.GetOpenedStoragesUseCase
@@ -10,6 +12,8 @@ import com.github.nullptroma.wallenc.usecases.ManageVaultUseCase
import com.github.nullptroma.wallenc.usecases.RemoveStorageUseCase
import com.github.nullptroma.wallenc.usecases.RenameStorageUseCase
import com.github.nullptroma.wallenc.usecases.StorageFileManagementUseCase
import com.github.nullptroma.wallenc.vault.contract.DescribedVault
import com.github.nullptroma.wallenc.vault.contract.VaultDescriptor
import com.github.nullptroma.wallenc.vault.contract.described
import com.github.nullptroma.wallenc.vault.contract.locals
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -36,6 +40,8 @@ class LocalVaultViewModel @Inject constructor(
storagesFlow = vaultsManager.vaults
.map { vaults -> vaults.described().locals.firstOrNull() }
.flatMapLatest { v -> v?.storages ?: flowOf(emptyList()) },
vaultHeaderFlow = vaultsManager.vaults
.map { vaults -> vaults.described().locals.firstOrNull().toLocalVaultBrowserHeader() },
vaultAvailabilityFlow = vaultsManager.vaults
.map { vaults -> vaults.described().locals.firstOrNull() }
.flatMapLatest { v -> v?.isAvailable ?: flowOf(false) },
@@ -50,3 +56,8 @@ class LocalVaultViewModel @Inject constructor(
uiStrings = uiStrings,
logger = logger,
)
private fun IVault?.toLocalVaultBrowserHeader(): VaultBrowserHeader? {
if ((this as? DescribedVault)?.descriptor !is VaultDescriptor.LocalDevice) return null
return VaultBrowserHeader(titleResId = R.string.screen_title_local_vault)
}

View File

@@ -2,8 +2,13 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.vault
import androidx.lifecycle.SavedStateHandle
import com.github.nullptroma.wallenc.domain.interfaces.ILogger
import com.github.nullptroma.wallenc.domain.interfaces.IVault
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
import com.github.nullptroma.wallenc.ui.R
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
import com.github.nullptroma.wallenc.vault.contract.CloudBrand
import com.github.nullptroma.wallenc.vault.contract.DescribedVault
import com.github.nullptroma.wallenc.vault.contract.VaultDescriptor
import com.github.nullptroma.wallenc.usecases.GetOpenedStoragesUseCase
import com.github.nullptroma.wallenc.usecases.ManageStoragesEncryptionUseCase
import com.github.nullptroma.wallenc.usecases.ManageVaultUseCase
@@ -14,6 +19,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import java.util.UUID
import javax.inject.Inject
@@ -32,6 +38,11 @@ class RemoteVaultViewModel @Inject constructor(
logger: ILogger,
) : AbstractVaultBrowserViewModel(
storagesFlow = manageVaultUseCase.storagesOf(savedStateHandle.requireVaultUuid()),
storagesScanInProgressFlow = manageVaultUseCase.storagesScanInProgressOf(
savedStateHandle.requireVaultUuid(),
),
vaultHeaderFlow = manageVaultUseCase.observe(savedStateHandle.requireVaultUuid())
.map { vault -> vault.toRemoteVaultBrowserHeader() },
vaultAvailabilityFlow = manageVaultUseCase.observe(savedStateHandle.requireVaultUuid())
.flatMapLatest { v -> v?.isAvailable ?: flowOf(false) },
resolveCreateVaultUuid = { savedStateHandle.requireVaultUuid() },
@@ -44,9 +55,25 @@ class RemoteVaultViewModel @Inject constructor(
taskOrchestrator = taskOrchestrator,
uiStrings = uiStrings,
logger = logger,
)
) {
override val supportsStorageRescan: Boolean = true
}
private fun SavedStateHandle.requireVaultUuid(): UUID {
val raw = get<String>("vaultUuid") ?: error("Missing vault UUID in navigation arguments")
return UUID.fromString(raw)
}
private fun IVault?.toRemoteVaultBrowserHeader(): VaultBrowserHeader? {
val remote = (this as? DescribedVault)?.descriptor as? VaultDescriptor.LinkedRemote ?: return null
val subtitle = when (remote.brand) {
CloudBrand.YANDEX -> remote.accountDisplayName
}
val titleResId = when (remote.brand) {
CloudBrand.YANDEX -> R.string.screen_title_yandex_vault
}
return VaultBrowserHeader(
titleResId = titleResId,
subtitle = subtitle,
)
}

View File

@@ -0,0 +1,8 @@
package com.github.nullptroma.wallenc.ui.screens.main.screens.vault
import androidx.annotation.StringRes
data class VaultBrowserHeader(
@param:StringRes val titleResId: Int,
val subtitle: String? = null,
)

View File

@@ -17,6 +17,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -31,7 +32,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@@ -42,6 +42,8 @@ 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,
@@ -78,6 +80,10 @@ fun VaultBrowserScreen(
val fabBusy = uiState.vaultListMutationActive
val showFullscreenLoader = uiState.storagesList.isEmpty() && uiState.storagesRefreshing
val showEmptyState = uiState.storagesList.isEmpty() && !uiState.storagesRefreshing
val showRescan = viewModel.supportsStorageRescan
val rescanEnabled = showRescan &&
!uiState.vaultListMutationActive &&
!uiState.storagesRefreshing
val isUuidBusy: (UUID) -> Boolean = { uuid -> uuid in uiState.busyStorageUuids }
val addFab: @Composable () -> Unit = {
@@ -103,78 +109,108 @@ fun VaultBrowserScreen(
}
val vaultContent: @Composable (androidx.compose.foundation.layout.PaddingValues) -> Unit = { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
) {
if (!fabEnabled) {
Text(
text = stringResource(R.string.vault_unavailable_banner),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 6.dp),
)
}
Box(
modifier = Modifier.fillMaxSize(),
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
) {
uiState.header?.let { header ->
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
) {
when {
showEmptyState -> {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(R.string.vault_empty_list_hint),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
Text(
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 (!fabEnabled) {
Text(
text = stringResource(R.string.vault_unavailable_banner),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 6.dp),
)
}
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
) {
when {
showEmptyState -> {
Text(
text = stringResource(
if (showRescan) {
R.string.vault_empty_list_hint_remote
} else {
R.string.vault_empty_list_hint
},
),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier
.align(Alignment.Center)
.padding(horizontal = 24.dp),
)
}
else -> {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
items(uiState.storagesList) { listItem ->
StorageTree(
modifier = Modifier.padding(8.dp, 8.dp, 8.dp, 0.dp),
tree = listItem,
isUuidBusy = isUuidBusy,
onClick = { onOpenStorageHome(it.value.uuid.toString()) },
onRename = { tree, newName -> viewModel.rename(tree.value, newName) },
onRemove = { tree -> viewModel.remove(tree.value) },
onEncrypt = { tree, password, encryptPath, rememberPassword ->
viewModel.enableEncryption(
tree.value,
password,
encryptPath,
rememberPassword,
)
},
onOpenEncrypted = { tree, password, remember ->
viewModel.openEncryptedStorage(tree.value, password, remember)
},
onCloseEncrypted = { tree -> viewModel.closeEncryptedStorage(tree.value) },
onDisableEncryption = { tree -> viewModel.disableEncryption(tree.value) },
getStatusTextRes = { tree -> viewModel.getStorageStatusRes(tree.value) },
isEncryptionOpened = { tree -> viewModel.isEncryptionSessionOpen(tree.value) },
isStorageSyncLockHeld = { info -> viewModel.isStorageSyncLockHeld(info) },
onClearStorageSyncLock = { info -> viewModel.clearStorageSyncLock(info) },
)
}
}
else -> {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
items(uiState.storagesList) { listItem ->
StorageTree(
modifier = Modifier.padding(8.dp, 8.dp, 8.dp, 0.dp),
tree = listItem,
isUuidBusy = isUuidBusy,
onClick = { onOpenStorageHome(it.value.uuid.toString()) },
onRename = { tree, newName -> viewModel.rename(tree.value, newName) },
onRemove = { tree -> viewModel.remove(tree.value) },
onEncrypt = { tree, password, encryptPath, rememberPassword ->
viewModel.enableEncryption(
tree.value,
password,
encryptPath,
rememberPassword,
)
},
onOpenEncrypted = { tree, password, remember ->
viewModel.openEncryptedStorage(tree.value, password, remember)
},
onCloseEncrypted = { tree -> viewModel.closeEncryptedStorage(tree.value) },
onDisableEncryption = { tree -> viewModel.disableEncryption(tree.value) },
getStatusTextRes = { tree -> viewModel.getStorageStatusRes(tree.value) },
isEncryptionOpened = { tree -> viewModel.isEncryptionSessionOpen(tree.value) },
isStorageSyncLockHeld = { info -> viewModel.isStorageSyncLockHeld(info) },
onClearStorageSyncLock = { info -> viewModel.clearStorageSyncLock(info) },
)
}
item { Spacer(modifier = Modifier.height(8.dp)) }
item {
Spacer(
modifier = Modifier.height(
if (showRescan) VaultRescanBottomInset else 8.dp,
),
)
}
}
}
}
}
}
}
Box(modifier = modifier) {
@@ -184,22 +220,38 @@ 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(modifier = Modifier.fillMaxSize().alpha(0.6f).background(Color.Black))
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.scrim),
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
CircularProgressIndicator(
modifier = Modifier.size(64.dp),
color = MaterialTheme.colorScheme.secondary,
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.surfaceVariant,
)
Text(
text = stringResource(R.string.vault_loading_storages),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onPrimary,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
)

View File

@@ -5,6 +5,7 @@ import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
import java.util.UUID
data class VaultBrowserScreenState(
val header: VaultBrowserHeader? = null,
val storagesList: List<Tree<IStorageInfo>>,
/** Первый снимок списка storages ещё не получен (удалённый vault). */
val storagesRefreshing: Boolean,

View File

@@ -8,6 +8,13 @@
<string name="nav_label_sync">Синхронизация</string>
<string name="nav_label_settings">Настройки</string>
<string name="nav_cd_back">Назад</string>
<string name="screen_title_remote_vault">Удалённое хранилище</string>
<string name="screen_title_local_vault">Локальное хранилище</string>
<string name="screen_title_yandex_vault">Хранилище Яндекс.Диска</string>
<string name="screen_title_storage">Хранилище</string>
<string name="screen_title_two_fa">Токены 2FA</string>
<string name="screen_title_text_secrets">Текстовые секреты</string>
<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>
@@ -85,9 +92,12 @@
<string name="vault_fab_add_storage_busy_cd">Создание хранилища уже выполняется</string>
<string name="vault_msg_storage_pipeline_busy">С этим хранилищем уже выполняется операция</string>
<string name="vault_msg_vault_list_mutation_busy">Список хранилищ сейчас меняется — подождите</string>
<string name="vault_msg_rescan_already_in_progress">Сканирование хранилищ уже выполняется</string>
<string name="vault_unavailable_banner">Хранилище недоступно. Проверьте сеть, путь или разблокировку.</string>
<string name="vault_loading_storages">Загрузка списка хранилищ…</string>
<string name="vault_empty_list_hint">В этом хранилище пока нет каталогов. Создайте хранилище кнопкой «+», когда оно доступно.</string>
<string name="vault_empty_list_hint_remote">На удалённом хранилище каталоги не найдены. Если папки уже есть на сервере, нажмите «Обновить список», либо создайте хранилище кнопкой «+», когда оно доступно.</string>
<string name="vault_rescan_storages_action">Обновить список</string>
<string name="task_pipeline_title">Очередь задач</string>
<string name="task_pipeline_jobs">Задачи</string>
<string name="task_pipeline_log">Журнал</string>
@@ -122,6 +132,7 @@
<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>
@@ -134,6 +145,7 @@
<string name="task_title_add_remote_vault">Добавление удалённого хранилища</string>
<string name="task_title_remove_remote_vault">Удаление удалённого хранилища</string>
<string name="task_title_retry_remote_vault">Повторное подключение удалённого хранилища</string>
<string name="task_title_rescan_vault_storages">Обновление списка хранилищ</string>
<string name="task_title_storage_sync">Синхронизация хранилищ</string>
<string name="task_title_storage_sync_background">Фоновая синхронизация хранилищ</string>
<string name="task_title_save_2fa_token">Сохранение 2FA токена</string>
@@ -315,6 +327,9 @@
<string name="task_log_retrying_vault">Повторное подключение…</string>
<string name="task_log_retry_requested">Повтор запрошен</string>
<string name="task_log_retry_vault_failed">Не удалось повторить подключение</string>
<string name="task_log_rescanning_vault_storages">Повторное сканирование хранилищ на удалённом vault…</string>
<string name="task_log_rescan_vault_storages_done">Список хранилищ обновлён</string>
<string name="task_log_rescan_vault_storages_failed">Не удалось обновить список хранилищ</string>
<string name="task_log_test_started">Тестовая задача запущена на %1$d с</string>
<string name="task_log_test_finished">Тестовая задача завершена</string>
</resources>

View File

@@ -9,6 +9,8 @@
<string name="nav_label_settings">Settings</string>
<string name="nav_cd_back">Go back</string>
<string name="screen_title_remote_vault">Remote vault</string>
<string name="screen_title_local_vault">Local vault</string>
<string name="screen_title_yandex_vault">Yandex Disk vault</string>
<string name="screen_title_storage">Storage</string>
<string name="screen_title_two_fa">2FA tokens</string>
<string name="screen_title_text_secrets">Text secrets</string>
@@ -90,9 +92,12 @@
<string name="vault_fab_add_storage_busy_cd">Storage creation already running</string>
<string name="vault_msg_storage_pipeline_busy">An operation is already running for this storage</string>
<string name="vault_msg_vault_list_mutation_busy">Storage list is changing — please wait</string>
<string name="vault_msg_rescan_already_in_progress">Storage scan is already in progress</string>
<string name="vault_unavailable_banner">Vault unavailable. Check network, path, or unlock.</string>
<string name="vault_loading_storages">Loading storage list…</string>
<string name="vault_empty_list_hint">No folders yet. Create storage with "+" when available.</string>
<string name="vault_empty_list_hint_remote">No storages found on the remote vault. Tap rescan if folders already exist on the server, or create one with "+" when available.</string>
<string name="vault_rescan_storages_action">Rescan storages</string>
<string name="task_pipeline_title">Task queue</string>
<string name="task_pipeline_jobs">Tasks</string>
<string name="task_pipeline_log">Log</string>
@@ -127,6 +132,7 @@
<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>
@@ -139,6 +145,7 @@
<string name="task_title_add_remote_vault">Add remote vault</string>
<string name="task_title_remove_remote_vault">Remove remote vault</string>
<string name="task_title_retry_remote_vault">Retry remote vault connection</string>
<string name="task_title_rescan_vault_storages">Rescan vault storages</string>
<string name="task_title_storage_sync">Storage sync</string>
<string name="task_title_storage_sync_background">Background storage sync</string>
<string name="task_title_save_2fa_token">Save 2FA token</string>
@@ -320,6 +327,9 @@
<string name="task_log_retrying_vault">Retrying remote vault connection…</string>
<string name="task_log_retry_requested">Retry requested</string>
<string name="task_log_retry_vault_failed">Failed to retry remote vault</string>
<string name="task_log_rescanning_vault_storages">Rescanning storages on remote vault…</string>
<string name="task_log_rescan_vault_storages_done">Storage list updated</string>
<string name="task_log_rescan_vault_storages_failed">Failed to rescan storages</string>
<string name="task_log_test_started">Test task started for %1$d s</string>
<string name="task_log_test_finished">Test task finished</string>
</resources>