Сильно улучшен UX при работе с Yandex vault
This commit is contained in:
@@ -3,6 +3,7 @@ package com.github.nullptroma.wallenc.domain.vault.storages.common
|
|||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.github.nullptroma.wallenc.domain.common.impl.CommonStorageMetaInfo
|
import com.github.nullptroma.wallenc.domain.common.impl.CommonStorageMetaInfo
|
||||||
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
|
||||||
|
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
|
||||||
@@ -70,17 +71,30 @@ abstract class BaseStorage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun readMetaInfo() = withContext(ioDispatcher) {
|
private suspend fun readMetaInfo() = withContext(ioDispatcher) {
|
||||||
var meta: CommonStorageMetaInfo
|
val meta = try {
|
||||||
var reader: InputStream? = null
|
accessor.openReadSystemFile(metaInfoFileName).use { input ->
|
||||||
try {
|
val bytes = input.readBytes()
|
||||||
reader = accessor.openReadSystemFile(metaInfoFileName)
|
when {
|
||||||
meta = jackson.readValue(reader, CommonStorageMetaInfo::class.java)
|
bytes.isEmpty() -> {
|
||||||
|
val default = CommonStorageMetaInfo()
|
||||||
|
updateMetaInfo(default)
|
||||||
|
default
|
||||||
|
}
|
||||||
|
else -> try {
|
||||||
|
jackson.readValue(bytes, CommonStorageMetaInfo::class.java)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// чтение не удалось — пишем дефолт, чтобы файл появился
|
// Битый JSON — не перезаписываем файл на диске
|
||||||
meta = CommonStorageMetaInfo()
|
CommonStorageMetaInfo()
|
||||||
updateMetaInfo(meta)
|
}
|
||||||
} finally {
|
}
|
||||||
reader?.close()
|
}
|
||||||
|
} catch (_: WallencException.Storage.FileNotFound) {
|
||||||
|
val default = CommonStorageMetaInfo()
|
||||||
|
updateMetaInfo(default)
|
||||||
|
default
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Сеть/IO — оставляем дефолт в памяти, существующий файл не трогаем
|
||||||
|
CommonStorageMetaInfo()
|
||||||
}
|
}
|
||||||
_metaInfo.value = meta
|
_metaInfo.value = meta
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,13 +272,7 @@ class EncryptedStorageAccessor(
|
|||||||
|
|
||||||
override suspend fun openReadSystemFile(name: String): InputStream = scope.run {
|
override suspend fun openReadSystemFile(name: String): InputStream = scope.run {
|
||||||
val path = Path(systemHiddenDirName, name).pathString
|
val path = Path(systemHiddenDirName, name).pathString
|
||||||
return@run try {
|
|
||||||
openRead(path)
|
openRead(path)
|
||||||
} catch (_: Exception) {
|
|
||||||
// Как у Yandex/Local: системного файла ещё нет — создаём пустой и читаем снова.
|
|
||||||
openWriteSystemFile(name).use { }
|
|
||||||
openRead(path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun openWriteSystemFile(name: String): OutputStream = scope.run {
|
override suspend fun openWriteSystemFile(name: String): OutputStream = scope.run {
|
||||||
|
|||||||
@@ -553,11 +553,9 @@ class LocalStorageAccessor(
|
|||||||
val dirPath = _filesystemBasePath.resolve(SYSTEM_HIDDEN_DIRNAME)
|
val dirPath = _filesystemBasePath.resolve(SYSTEM_HIDDEN_DIRNAME)
|
||||||
val path = dirPath.resolve(name)
|
val path = dirPath.resolve(name)
|
||||||
val file = path.toFile()
|
val file = path.toFile()
|
||||||
if(!file.exists()) {
|
if (!file.exists()) {
|
||||||
Files.createDirectories(dirPath)
|
throw WallencException.Storage.FileNotFound()
|
||||||
file.createNewFile()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return@withContext file.inputStream()
|
return@withContext file.inputStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -575,12 +575,11 @@ class YandexStorageAccessor(
|
|||||||
override suspend fun openReadSystemFile(name: String): InputStream = withContext(ioDispatcher) {
|
override suspend fun openReadSystemFile(name: String): InputStream = withContext(ioDispatcher) {
|
||||||
ensureSystemDirExists()
|
ensureSystemDirExists()
|
||||||
val rel = "/$SYSTEM_HIDDEN_DIRNAME/$name"
|
val rel = "/$SYSTEM_HIDDEN_DIRNAME/$name"
|
||||||
try {
|
val diskPath = toDiskPath(rel)
|
||||||
guard { repo.openDownloadStream(toDiskPath(rel)) }
|
when (guard { repo.getOrNull(diskPath) }?.type) {
|
||||||
} catch (_: Exception) {
|
"file" -> guard { repo.openDownloadStream(diskPath) }
|
||||||
// как Local: пустой файл если нет
|
null -> throw WallencException.Storage.FileNotFound()
|
||||||
guard { repo.uploadBytes(toDiskPath(rel), ByteArray(0), overwrite = true) }
|
else -> throw WallencException.Storage.FileNotFound()
|
||||||
guard { repo.openDownloadStream(toDiskPath(rel)) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,17 @@ class LocalVault(
|
|||||||
return@withContext storage
|
return@withContext storage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun rescanStorages() = withContext(ioDispatcher) {
|
||||||
|
_storagesScanInProgress.value = true
|
||||||
|
try {
|
||||||
|
if (_isAvailable.value) {
|
||||||
|
readStorages()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_storagesScanInProgress.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {
|
override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {
|
||||||
val path = path.value
|
val path = path.value
|
||||||
if (path == null || !_isAvailable.value) {
|
if (path == null || !_isAvailable.value) {
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -54,12 +57,20 @@ class YandexVault(
|
|||||||
private val _availableSpace = MutableStateFlow<Long?>(null)
|
private val _availableSpace = MutableStateFlow<Long?>(null)
|
||||||
override val availableSpace: StateFlow<Long?> = _availableSpace
|
override val availableSpace: StateFlow<Long?> = _availableSpace
|
||||||
|
|
||||||
|
private val refreshMutex = Mutex()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
parentScope.launch {
|
parentScope.launch {
|
||||||
runCatching { refreshFromDisk() }
|
runCatching { refreshFromDisk() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun rescanStorages() {
|
||||||
|
refreshMutex.withLock {
|
||||||
|
refreshFromDisk()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun refreshFromDisk() {
|
private suspend fun refreshFromDisk() {
|
||||||
_storagesScanInProgress.value = true
|
_storagesScanInProgress.value = true
|
||||||
_vaultReachable.value = false
|
_vaultReachable.value = false
|
||||||
@@ -111,13 +122,23 @@ class YandexVault(
|
|||||||
if (pending.isEmpty()) return emptyList()
|
if (pending.isEmpty()) return emptyList()
|
||||||
return coroutineScope {
|
return coroutineScope {
|
||||||
pending.map { storage ->
|
pending.map { storage ->
|
||||||
async(ioDispatcher) {
|
async(ioDispatcher) { initStorageWithRetry(storage) }
|
||||||
if (runCatching { storage.init() }.isSuccess) storage else null
|
|
||||||
}
|
|
||||||
}.awaitAll().filterNotNull()
|
}.awaitAll().filterNotNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun initStorageWithRetry(storage: YandexStorage): YandexStorage? {
|
||||||
|
for (attempt in 0 until STORAGE_INIT_ATTEMPTS) {
|
||||||
|
if (attempt > 0) {
|
||||||
|
delay(STORAGE_INIT_RETRY_DELAY_MS * attempt)
|
||||||
|
}
|
||||||
|
if (runCatching { storage.init() }.isSuccess) {
|
||||||
|
return storage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun createStorage(): IStorage = withContext(ioDispatcher) {
|
override suspend fun createStorage(): IStorage = withContext(ioDispatcher) {
|
||||||
val id = UUID.randomUUID()
|
val id = UUID.randomUUID()
|
||||||
repo.createFolder("app:/$id")
|
repo.createFolder("app:/$id")
|
||||||
@@ -150,5 +171,7 @@ class YandexVault(
|
|||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private const val APP_LIST_LIMIT = 1000
|
private const val APP_LIST_LIMIT = 1000
|
||||||
|
private const val STORAGE_INIT_ATTEMPTS = 3
|
||||||
|
private const val STORAGE_INIT_RETRY_DELAY_MS = 400L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,4 +21,7 @@ interface IVault : IVaultInfo {
|
|||||||
suspend fun createStorage(): IStorage
|
suspend fun createStorage(): IStorage
|
||||||
suspend fun createStorage(enc: StorageEncryptionInfo): IStorage
|
suspend fun createStorage(enc: StorageEncryptionInfo): IStorage
|
||||||
suspend fun remove(storage: IStorage)
|
suspend fun remove(storage: IStorage)
|
||||||
|
|
||||||
|
/** Пересканировать список storages (для удалённых vault — повторный листинг и init). */
|
||||||
|
suspend fun rescanStorages() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ enum class VaultTaskStep {
|
|||||||
AddRemoteVault,
|
AddRemoteVault,
|
||||||
RemoveRemoteVault,
|
RemoveRemoteVault,
|
||||||
RetryRemoteVault,
|
RetryRemoteVault,
|
||||||
|
RescanVaultStorages,
|
||||||
Save2FaToken,
|
Save2FaToken,
|
||||||
Delete2FaToken,
|
Delete2FaToken,
|
||||||
SaveTextSecret,
|
SaveTextSecret,
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ fun TaskProgressLabel.resolve(resolver: UiStringResolver): String = when (this)
|
|||||||
VaultTaskStep.AddRemoteVault -> resolver(R.string.task_progress_add_remote_vault)
|
VaultTaskStep.AddRemoteVault -> resolver(R.string.task_progress_add_remote_vault)
|
||||||
VaultTaskStep.RemoveRemoteVault -> resolver(R.string.task_progress_remove_remote_vault)
|
VaultTaskStep.RemoveRemoteVault -> resolver(R.string.task_progress_remove_remote_vault)
|
||||||
VaultTaskStep.RetryRemoteVault -> resolver(R.string.task_progress_retry_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.Save2FaToken -> resolver(R.string.task_progress_save_2fa_token)
|
||||||
VaultTaskStep.Delete2FaToken -> resolver(R.string.task_progress_delete_2fa_token)
|
VaultTaskStep.Delete2FaToken -> resolver(R.string.task_progress_delete_2fa_token)
|
||||||
VaultTaskStep.SaveTextSecret -> resolver(R.string.task_progress_save_text_secret)
|
VaultTaskStep.SaveTextSecret -> resolver(R.string.task_progress_save_text_secret)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage
|
package com.github.nullptroma.wallenc.ui.screens.main.screens.storage
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
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.Row
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -18,6 +19,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -45,7 +47,12 @@ fun StorageHomeScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
if (uiState.isLoading) {
|
if (uiState.isLoading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
return@Column
|
return@Column
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -39,6 +40,8 @@ import java.util.UUID
|
|||||||
*/
|
*/
|
||||||
abstract class AbstractVaultBrowserViewModel(
|
abstract class AbstractVaultBrowserViewModel(
|
||||||
storagesFlow: Flow<List<IStorage>>,
|
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 vaultAvailabilityFlow: Flow<Boolean>,
|
||||||
private val resolveCreateVaultUuid: () -> UUID?,
|
private val resolveCreateVaultUuid: () -> UUID?,
|
||||||
private val removeStorageUseCase: RemoveStorageUseCase,
|
private val removeStorageUseCase: RemoveStorageUseCase,
|
||||||
@@ -63,8 +66,12 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
private val _userNotifications = MutableSharedFlow<UserNotification>(extraBufferCapacity = 8)
|
private val _userNotifications = MutableSharedFlow<UserNotification>(extraBufferCapacity = 8)
|
||||||
val userNotifications: SharedFlow<UserNotification> = _userNotifications
|
val userNotifications: SharedFlow<UserNotification> = _userNotifications
|
||||||
|
|
||||||
|
/** Удалённый vault: показать кнопку повторного сканирования storages на Диске. */
|
||||||
|
open val supportsStorageRescan: Boolean = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
collectStoragesFlow(storagesFlow)
|
collectStoragesFlow(storagesFlow)
|
||||||
|
collectVaultHeaderFlow()
|
||||||
collectPipelineBusyFlags()
|
collectPipelineBusyFlags()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
vaultAvailabilityFlow
|
vaultAvailabilityFlow
|
||||||
@@ -94,13 +101,23 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
t.locksVaultStorageList && isPipelineTaskActive(t.state)
|
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>>) {
|
private fun collectStoragesFlow(storagesFlow: Flow<List<IStorage>>) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
combine(
|
combine(
|
||||||
storagesFlow,
|
storagesFlow,
|
||||||
|
storagesScanInProgressFlow,
|
||||||
getOpenedStoragesUseCase.openedStorages,
|
getOpenedStoragesUseCase.openedStorages,
|
||||||
) { storages, opened -> storages to opened }
|
) { storages, scanInProgress, opened ->
|
||||||
.collect { (storages, opened) ->
|
Triple(storages, scanInProgress, opened)
|
||||||
|
}.collect { (storages, scanInProgress, opened) ->
|
||||||
val list = mutableListOf<Tree<IStorageInfo>>()
|
val list = mutableListOf<Tree<IStorageInfo>>()
|
||||||
for (storage in storages) {
|
for (storage in storages) {
|
||||||
var tree = Tree<IStorageInfo>(storage)
|
var tree = Tree<IStorageInfo>(storage)
|
||||||
@@ -115,7 +132,7 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
storagesList = list,
|
storagesList = list,
|
||||||
storagesRefreshing = false,
|
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() {
|
fun createStorage() {
|
||||||
if (!state.value.addStorageFabEnabled) {
|
if (!state.value.addStorageFabEnabled) {
|
||||||
logger.debug(TAG, "createStorage ignored (vault unavailable or FAB disabled)")
|
logger.debug(TAG, "createStorage ignored (vault unavailable or FAB disabled)")
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.github.nullptroma.wallenc.ui.screens.main.screens.vault
|
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.ILogger
|
||||||
|
import com.github.nullptroma.wallenc.domain.interfaces.IVault
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
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.domain.tasks.ITaskOrchestrator
|
||||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||||
import com.github.nullptroma.wallenc.usecases.GetOpenedStoragesUseCase
|
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.RemoveStorageUseCase
|
||||||
import com.github.nullptroma.wallenc.usecases.RenameStorageUseCase
|
import com.github.nullptroma.wallenc.usecases.RenameStorageUseCase
|
||||||
import com.github.nullptroma.wallenc.usecases.StorageFileManagementUseCase
|
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.described
|
||||||
import com.github.nullptroma.wallenc.vault.contract.locals
|
import com.github.nullptroma.wallenc.vault.contract.locals
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@@ -36,6 +40,8 @@ class LocalVaultViewModel @Inject constructor(
|
|||||||
storagesFlow = vaultsManager.vaults
|
storagesFlow = vaultsManager.vaults
|
||||||
.map { vaults -> vaults.described().locals.firstOrNull() }
|
.map { vaults -> vaults.described().locals.firstOrNull() }
|
||||||
.flatMapLatest { v -> v?.storages ?: flowOf(emptyList()) },
|
.flatMapLatest { v -> v?.storages ?: flowOf(emptyList()) },
|
||||||
|
vaultHeaderFlow = vaultsManager.vaults
|
||||||
|
.map { vaults -> vaults.described().locals.firstOrNull().toLocalVaultBrowserHeader() },
|
||||||
vaultAvailabilityFlow = vaultsManager.vaults
|
vaultAvailabilityFlow = vaultsManager.vaults
|
||||||
.map { vaults -> vaults.described().locals.firstOrNull() }
|
.map { vaults -> vaults.described().locals.firstOrNull() }
|
||||||
.flatMapLatest { v -> v?.isAvailable ?: flowOf(false) },
|
.flatMapLatest { v -> v?.isAvailable ?: flowOf(false) },
|
||||||
@@ -50,3 +56,8 @@ class LocalVaultViewModel @Inject constructor(
|
|||||||
uiStrings = uiStrings,
|
uiStrings = uiStrings,
|
||||||
logger = logger,
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,13 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.vault
|
|||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.ILogger
|
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.domain.tasks.ITaskOrchestrator
|
||||||
|
import com.github.nullptroma.wallenc.ui.R
|
||||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
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.GetOpenedStoragesUseCase
|
||||||
import com.github.nullptroma.wallenc.usecases.ManageStoragesEncryptionUseCase
|
import com.github.nullptroma.wallenc.usecases.ManageStoragesEncryptionUseCase
|
||||||
import com.github.nullptroma.wallenc.usecases.ManageVaultUseCase
|
import com.github.nullptroma.wallenc.usecases.ManageVaultUseCase
|
||||||
@@ -14,6 +19,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
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 java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -32,6 +38,11 @@ class RemoteVaultViewModel @Inject constructor(
|
|||||||
logger: ILogger,
|
logger: ILogger,
|
||||||
) : AbstractVaultBrowserViewModel(
|
) : AbstractVaultBrowserViewModel(
|
||||||
storagesFlow = manageVaultUseCase.storagesOf(savedStateHandle.requireVaultUuid()),
|
storagesFlow = manageVaultUseCase.storagesOf(savedStateHandle.requireVaultUuid()),
|
||||||
|
storagesScanInProgressFlow = manageVaultUseCase.storagesScanInProgressOf(
|
||||||
|
savedStateHandle.requireVaultUuid(),
|
||||||
|
),
|
||||||
|
vaultHeaderFlow = manageVaultUseCase.observe(savedStateHandle.requireVaultUuid())
|
||||||
|
.map { vault -> vault.toRemoteVaultBrowserHeader() },
|
||||||
vaultAvailabilityFlow = manageVaultUseCase.observe(savedStateHandle.requireVaultUuid())
|
vaultAvailabilityFlow = manageVaultUseCase.observe(savedStateHandle.requireVaultUuid())
|
||||||
.flatMapLatest { v -> v?.isAvailable ?: flowOf(false) },
|
.flatMapLatest { v -> v?.isAvailable ?: flowOf(false) },
|
||||||
resolveCreateVaultUuid = { savedStateHandle.requireVaultUuid() },
|
resolveCreateVaultUuid = { savedStateHandle.requireVaultUuid() },
|
||||||
@@ -44,9 +55,25 @@ class RemoteVaultViewModel @Inject constructor(
|
|||||||
taskOrchestrator = taskOrchestrator,
|
taskOrchestrator = taskOrchestrator,
|
||||||
uiStrings = uiStrings,
|
uiStrings = uiStrings,
|
||||||
logger = logger,
|
logger = logger,
|
||||||
)
|
) {
|
||||||
|
override val supportsStorageRescan: Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
private fun SavedStateHandle.requireVaultUuid(): UUID {
|
private fun SavedStateHandle.requireVaultUuid(): UUID {
|
||||||
val raw = get<String>("vaultUuid") ?: error("Missing vault UUID in navigation arguments")
|
val raw = get<String>("vaultUuid") ?: error("Missing vault UUID in navigation arguments")
|
||||||
return UUID.fromString(raw)
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -17,6 +17,7 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -31,7 +32,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
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 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,
|
||||||
@@ -78,6 +80,10 @@ fun VaultBrowserScreen(
|
|||||||
val fabBusy = uiState.vaultListMutationActive
|
val fabBusy = uiState.vaultListMutationActive
|
||||||
val showFullscreenLoader = uiState.storagesList.isEmpty() && uiState.storagesRefreshing
|
val showFullscreenLoader = uiState.storagesList.isEmpty() && uiState.storagesRefreshing
|
||||||
val showEmptyState = 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 isUuidBusy: (UUID) -> Boolean = { uuid -> uuid in uiState.busyStorageUuids }
|
||||||
|
|
||||||
val addFab: @Composable () -> Unit = {
|
val addFab: @Composable () -> Unit = {
|
||||||
@@ -108,6 +114,27 @@ fun VaultBrowserScreen(
|
|||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
|
uiState.header?.let { header ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
) {
|
||||||
|
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) {
|
if (!fabEnabled) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.vault_unavailable_banner),
|
text = stringResource(R.string.vault_unavailable_banner),
|
||||||
@@ -119,25 +146,28 @@ fun VaultBrowserScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
showEmptyState -> {
|
showEmptyState -> {
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(24.dp),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.vault_empty_list_hint),
|
text = stringResource(
|
||||||
|
if (showRescan) {
|
||||||
|
R.string.vault_empty_list_hint_remote
|
||||||
|
} else {
|
||||||
|
R.string.vault_empty_list_hint
|
||||||
|
},
|
||||||
|
),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.padding(horizontal = 24.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else -> {
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -169,7 +199,13 @@ fun VaultBrowserScreen(
|
|||||||
onClearStorageSyncLock = { info -> viewModel.clearStorageSyncLock(info) },
|
onClearStorageSyncLock = { info -> viewModel.clearStorageSyncLock(info) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item { Spacer(modifier = Modifier.height(8.dp)) }
|
item {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier.height(
|
||||||
|
if (showRescan) VaultRescanBottomInset else 8.dp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,22 +220,38 @@ 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(modifier = Modifier.fillMaxSize().alpha(0.6f).background(Color.Black))
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.scrim),
|
||||||
|
)
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.size(64.dp),
|
modifier = Modifier.size(64.dp),
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
trackColor = MaterialTheme.colorScheme.surfaceVariant,
|
trackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.vault_loading_storages),
|
text = stringResource(R.string.vault_loading_storages),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(horizontal = 24.dp),
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
data class VaultBrowserScreenState(
|
data class VaultBrowserScreenState(
|
||||||
|
val header: VaultBrowserHeader? = null,
|
||||||
val storagesList: List<Tree<IStorageInfo>>,
|
val storagesList: List<Tree<IStorageInfo>>,
|
||||||
/** Первый снимок списка storages ещё не получен (удалённый vault). */
|
/** Первый снимок списка storages ещё не получен (удалённый vault). */
|
||||||
val storagesRefreshing: Boolean,
|
val storagesRefreshing: Boolean,
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
<string name="nav_label_sync">Синхронизация</string>
|
<string name="nav_label_sync">Синхронизация</string>
|
||||||
<string name="nav_label_settings">Настройки</string>
|
<string name="nav_label_settings">Настройки</string>
|
||||||
<string name="nav_cd_back">Назад</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_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>
|
||||||
@@ -85,9 +92,12 @@
|
|||||||
<string name="vault_fab_add_storage_busy_cd">Создание хранилища уже выполняется</string>
|
<string name="vault_fab_add_storage_busy_cd">Создание хранилища уже выполняется</string>
|
||||||
<string name="vault_msg_storage_pipeline_busy">С этим хранилищем уже выполняется операция</string>
|
<string name="vault_msg_storage_pipeline_busy">С этим хранилищем уже выполняется операция</string>
|
||||||
<string name="vault_msg_vault_list_mutation_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_unavailable_banner">Хранилище недоступно. Проверьте сеть, путь или разблокировку.</string>
|
||||||
<string name="vault_loading_storages">Загрузка списка хранилищ…</string>
|
<string name="vault_loading_storages">Загрузка списка хранилищ…</string>
|
||||||
<string name="vault_empty_list_hint">В этом хранилище пока нет каталогов. Создайте хранилище кнопкой «+», когда оно доступно.</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_title">Очередь задач</string>
|
||||||
<string name="task_pipeline_jobs">Задачи</string>
|
<string name="task_pipeline_jobs">Задачи</string>
|
||||||
<string name="task_pipeline_log">Журнал</string>
|
<string name="task_pipeline_log">Журнал</string>
|
||||||
@@ -122,6 +132,7 @@
|
|||||||
<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_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>
|
||||||
@@ -134,6 +145,7 @@
|
|||||||
<string name="task_title_add_remote_vault">Добавление удалённого хранилища</string>
|
<string name="task_title_add_remote_vault">Добавление удалённого хранилища</string>
|
||||||
<string name="task_title_remove_remote_vault">Удаление удалённого хранилища</string>
|
<string name="task_title_remove_remote_vault">Удаление удалённого хранилища</string>
|
||||||
<string name="task_title_retry_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">Синхронизация хранилищ</string>
|
||||||
<string name="task_title_storage_sync_background">Фоновая синхронизация хранилищ</string>
|
<string name="task_title_storage_sync_background">Фоновая синхронизация хранилищ</string>
|
||||||
<string name="task_title_save_2fa_token">Сохранение 2FA токена</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_retrying_vault">Повторное подключение…</string>
|
||||||
<string name="task_log_retry_requested">Повтор запрошен</string>
|
<string name="task_log_retry_requested">Повтор запрошен</string>
|
||||||
<string name="task_log_retry_vault_failed">Не удалось повторить подключение</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_started">Тестовая задача запущена на %1$d с</string>
|
||||||
<string name="task_log_test_finished">Тестовая задача завершена</string>
|
<string name="task_log_test_finished">Тестовая задача завершена</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
<string name="nav_label_settings">Settings</string>
|
<string name="nav_label_settings">Settings</string>
|
||||||
<string name="nav_cd_back">Go back</string>
|
<string name="nav_cd_back">Go back</string>
|
||||||
<string name="screen_title_remote_vault">Remote vault</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_storage">Storage</string>
|
||||||
<string name="screen_title_two_fa">2FA tokens</string>
|
<string name="screen_title_two_fa">2FA tokens</string>
|
||||||
<string name="screen_title_text_secrets">Text secrets</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_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_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_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_unavailable_banner">Vault unavailable. Check network, path, or unlock.</string>
|
||||||
<string name="vault_loading_storages">Loading storage list…</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">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_title">Task queue</string>
|
||||||
<string name="task_pipeline_jobs">Tasks</string>
|
<string name="task_pipeline_jobs">Tasks</string>
|
||||||
<string name="task_pipeline_log">Log</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_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_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>
|
||||||
@@ -139,6 +145,7 @@
|
|||||||
<string name="task_title_add_remote_vault">Add remote vault</string>
|
<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_remove_remote_vault">Remove remote vault</string>
|
||||||
<string name="task_title_retry_remote_vault">Retry remote vault connection</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">Storage sync</string>
|
||||||
<string name="task_title_storage_sync_background">Background 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>
|
<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_retrying_vault">Retrying remote vault connection…</string>
|
||||||
<string name="task_log_retry_requested">Retry requested</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_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_started">Test task started for %1$d s</string>
|
||||||
<string name="task_log_test_finished">Test task finished</string>
|
<string name="task_log_test_finished">Test task finished</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -30,10 +30,21 @@ class ManageVaultUseCase @Inject constructor(
|
|||||||
fun storagesOf(vaultUuid: UUID): Flow<List<IStorage>> =
|
fun storagesOf(vaultUuid: UUID): Flow<List<IStorage>> =
|
||||||
observe(vaultUuid).flatMapLatest { vault -> vault?.storages ?: flowOf(emptyList()) }
|
observe(vaultUuid).flatMapLatest { vault -> vault?.storages ?: flowOf(emptyList()) }
|
||||||
|
|
||||||
|
/** Идёт листинг/пересканирование storages vault'а. */
|
||||||
|
fun storagesScanInProgressOf(vaultUuid: UUID): Flow<Boolean> =
|
||||||
|
observe(vaultUuid).flatMapLatest { vault -> vault?.storagesScanInProgress ?: flowOf(false) }
|
||||||
|
|
||||||
/** Создать новое хранилище в указанном vault'е. */
|
/** Создать новое хранилище в указанном vault'е. */
|
||||||
suspend fun createStorage(vaultUuid: UUID): IStorage {
|
suspend fun createStorage(vaultUuid: UUID): IStorage {
|
||||||
val vault = find(vaultUuid)
|
val vault = find(vaultUuid)
|
||||||
?: throw IllegalStateException("Vault $vaultUuid is not registered")
|
?: throw IllegalStateException("Vault $vaultUuid is not registered")
|
||||||
return vault.createStorage()
|
return vault.createStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Пересканировать storages vault'а (листинг на Диске и повторный init). */
|
||||||
|
suspend fun rescanStorages(vaultUuid: UUID) {
|
||||||
|
val vault = find(vaultUuid)
|
||||||
|
?: throw IllegalStateException("Vault $vaultUuid is not registered")
|
||||||
|
vault.rescanStorages()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user