Работа с 2fa и секретами перенесена в tasks
This commit is contained in:
@@ -59,7 +59,10 @@ fun TextSecretDetailsScreen(
|
||||
text = secret.title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
TextButton(onClick = { onEdit(secret.id) }, enabled = uiState.isAvailable) {
|
||||
TextButton(
|
||||
onClick = { onEdit(secret.id) },
|
||||
enabled = uiState.isAvailable && !uiState.isMutating,
|
||||
) {
|
||||
Text(stringResource(R.string.edit))
|
||||
}
|
||||
}
|
||||
@@ -94,7 +97,7 @@ fun TextSecretDetailsScreen(
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.delete(onDeleted) },
|
||||
enabled = uiState.isAvailable,
|
||||
enabled = uiState.isAvailable && !uiState.isMutating,
|
||||
) {
|
||||
Text(stringResource(R.string.remove))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
|
||||
data class TextSecretDetailsScreenState(
|
||||
val isLoading: Boolean = true,
|
||||
val isAvailable: Boolean = false,
|
||||
val isMutating: Boolean = false,
|
||||
val secret: TextSecretRecord? = null,
|
||||
val errorMessage: String? = null,
|
||||
)
|
||||
|
||||
@@ -2,11 +2,17 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.secrets
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
|
||||
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.ManageTextSecretsUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -17,6 +23,8 @@ class TextSecretDetailsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val findStorageUseCase: FindStorageUseCase,
|
||||
private val manageTextSecretsUseCase: ManageTextSecretsUseCase,
|
||||
private val taskOrchestrator: ITaskOrchestrator,
|
||||
private val uiStrings: UiStringResolver,
|
||||
) : ViewModelBase<TextSecretDetailsScreenState>(TextSecretDetailsScreenState()) {
|
||||
|
||||
private val storageUuid = savedStateHandle.requireStorageUuid()
|
||||
@@ -44,10 +52,16 @@ class TextSecretDetailsViewModel @Inject constructor(
|
||||
manageTextSecretsUseCase.observe(storage).map { list ->
|
||||
list.firstOrNull { it.id == secretId }
|
||||
},
|
||||
) { available, secret ->
|
||||
taskOrchestrator.pipelineState.map { pipe ->
|
||||
pipe.tasks.any { t ->
|
||||
t.busyStorageUuid == storage.uuid && isTaskActive(t.state)
|
||||
}
|
||||
},
|
||||
) { available, secret, isMutating ->
|
||||
state.value.copy(
|
||||
isLoading = false,
|
||||
isAvailable = available,
|
||||
isMutating = isMutating,
|
||||
secret = secret,
|
||||
errorMessage = if (secret == null) "Secret not found" else null,
|
||||
)
|
||||
@@ -60,10 +74,28 @@ class TextSecretDetailsViewModel @Inject constructor(
|
||||
fun delete(onDeleted: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
val storage = findStorageUseCase.find(storageUuid) ?: return@launch
|
||||
val result = CompletableDeferred<Result<Boolean>>()
|
||||
taskOrchestrator.enqueue(
|
||||
title = uiStrings(R.string.task_title_delete_text_secret),
|
||||
dispatcher = Dispatchers.IO,
|
||||
busyStorageUuid = storage.uuid,
|
||||
work = { _ ->
|
||||
try {
|
||||
val removed = manageTextSecretsUseCase.delete(storage, secretId)
|
||||
result.complete(Result.success(removed))
|
||||
} catch (t: Throwable) {
|
||||
result.complete(Result.failure(t))
|
||||
throw t
|
||||
}
|
||||
},
|
||||
)
|
||||
val removed = result.await().getOrElse { false }
|
||||
if (removed) {
|
||||
onDeleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTaskActive(state: TaskRunState): Boolean =
|
||||
state is TaskRunState.Queued || state is TaskRunState.Running
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ fun TextSecretEditScreen(
|
||||
label = { Text(stringResource(R.string.text_secret_item_value)) },
|
||||
)
|
||||
IconButton(
|
||||
enabled = !uiState.isMutating,
|
||||
onClick = {
|
||||
if (items.size > 1) {
|
||||
items.removeAt(index)
|
||||
@@ -121,7 +122,10 @@ fun TextSecretEditScreen(
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
TextButton(onClick = { items.add(TextSecretEntryRecord(label = null, value = "")) }) {
|
||||
TextButton(
|
||||
onClick = { items.add(TextSecretEntryRecord(label = null, value = "")) },
|
||||
enabled = !uiState.isMutating,
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Text(stringResource(R.string.text_secret_add_item))
|
||||
}
|
||||
@@ -133,7 +137,7 @@ fun TextSecretEditScreen(
|
||||
onSaved = currentOnSaved,
|
||||
)
|
||||
},
|
||||
enabled = title.isNotBlank(),
|
||||
enabled = title.isNotBlank() && !uiState.isMutating,
|
||||
) {
|
||||
Text(stringResource(R.string.save))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
|
||||
data class TextSecretEditScreenState(
|
||||
val isLoading: Boolean = true,
|
||||
val isAvailable: Boolean = false,
|
||||
val isMutating: Boolean = false,
|
||||
val initialSecret: TextSecretRecord? = null,
|
||||
val errorMessage: String? = null,
|
||||
)
|
||||
|
||||
@@ -4,13 +4,21 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretEntryRecord
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.optionalSecretId
|
||||
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
|
||||
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.ManageTextSecretsUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -19,6 +27,8 @@ class TextSecretEditViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val findStorageUseCase: FindStorageUseCase,
|
||||
private val manageTextSecretsUseCase: ManageTextSecretsUseCase,
|
||||
private val taskOrchestrator: ITaskOrchestrator,
|
||||
private val uiStrings: UiStringResolver,
|
||||
) : ViewModelBase<TextSecretEditScreenState>(TextSecretEditScreenState()) {
|
||||
|
||||
private val storageUuid = savedStateHandle.requireStorageUuid()
|
||||
@@ -40,15 +50,26 @@ class TextSecretEditViewModel @Inject constructor(
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
val initial = secretId?.let { manageTextSecretsUseCase.get(storage, it) }
|
||||
updateState(
|
||||
val initial = secretId?.let { id -> manageTextSecretsUseCase.get(storage, id) }
|
||||
combine(
|
||||
storage.isAvailable,
|
||||
taskOrchestrator.pipelineState.map { pipe ->
|
||||
pipe.tasks.any { t ->
|
||||
t.busyStorageUuid == storage.uuid && isTaskActive(t.state)
|
||||
}
|
||||
},
|
||||
flowOf(initial),
|
||||
) { available, isMutating, currentSecret ->
|
||||
state.value.copy(
|
||||
isLoading = false,
|
||||
isAvailable = storage.isAvailable.first(),
|
||||
initialSecret = initial,
|
||||
isAvailable = available,
|
||||
isMutating = isMutating,
|
||||
initialSecret = currentSecret,
|
||||
errorMessage = null,
|
||||
),
|
||||
)
|
||||
}.collect { ui ->
|
||||
updateState(ui)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,13 +81,20 @@ class TextSecretEditViewModel @Inject constructor(
|
||||
viewModelScope.launch {
|
||||
val storage = findStorageUseCase.find(storageUuid) ?: return@launch
|
||||
val existingId = secretId
|
||||
val result = CompletableDeferred<Result<String>>()
|
||||
taskOrchestrator.enqueue(
|
||||
title = uiStrings(R.string.task_title_save_text_secret),
|
||||
dispatcher = Dispatchers.IO,
|
||||
busyStorageUuid = storage.uuid,
|
||||
work = { _ ->
|
||||
try {
|
||||
if (existingId == null) {
|
||||
val created = manageTextSecretsUseCase.create(
|
||||
storageInfo = storage,
|
||||
title = title,
|
||||
items = items,
|
||||
)
|
||||
onSaved(created.id)
|
||||
result.complete(Result.success(created.id))
|
||||
} else {
|
||||
manageTextSecretsUseCase.update(
|
||||
storageInfo = storage,
|
||||
@@ -76,8 +104,19 @@ class TextSecretEditViewModel @Inject constructor(
|
||||
items = items,
|
||||
),
|
||||
)
|
||||
onSaved(existingId)
|
||||
}
|
||||
result.complete(Result.success(existingId))
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
result.complete(Result.failure(t))
|
||||
throw t
|
||||
}
|
||||
},
|
||||
)
|
||||
val savedId = result.await().getOrNull() ?: return@launch
|
||||
onSaved(savedId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTaskActive(state: TaskRunState): Boolean =
|
||||
state is TaskRunState.Queued || state is TaskRunState.Running
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ fun TwoFaTokensScreen(
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (uiState.isAvailable) {
|
||||
if (uiState.isAvailable && !uiState.isMutating) {
|
||||
creating = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier.alpha(if (uiState.isAvailable) 1f else 0.5f),
|
||||
modifier = Modifier.alpha(if (uiState.isAvailable && !uiState.isMutating) 1f else 0.5f),
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.two_fa_add_token))
|
||||
}
|
||||
@@ -133,13 +133,13 @@ fun TwoFaTokensScreen(
|
||||
Row {
|
||||
IconButton(
|
||||
onClick = { editingToken = item },
|
||||
enabled = uiState.isAvailable,
|
||||
enabled = uiState.isAvailable && !uiState.isMutating,
|
||||
) {
|
||||
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.edit))
|
||||
}
|
||||
IconButton(
|
||||
onClick = { viewModel.deleteToken(item.id) },
|
||||
enabled = uiState.isAvailable,
|
||||
enabled = uiState.isAvailable && !uiState.isMutating,
|
||||
) {
|
||||
Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.remove))
|
||||
}
|
||||
@@ -155,6 +155,7 @@ fun TwoFaTokensScreen(
|
||||
if (creating) {
|
||||
TwoFaTokenEditDialog(
|
||||
startValue = null,
|
||||
isBusy = uiState.isMutating,
|
||||
onDismiss = { creating = false },
|
||||
onSave = { issuer, account, secret, notes ->
|
||||
creating = false
|
||||
@@ -172,6 +173,7 @@ fun TwoFaTokensScreen(
|
||||
editingToken?.let { token ->
|
||||
TwoFaTokenEditDialog(
|
||||
startValue = token,
|
||||
isBusy = uiState.isMutating,
|
||||
onDismiss = { editingToken = null },
|
||||
onSave = { issuer, account, secret, notes ->
|
||||
editingToken = null
|
||||
@@ -190,6 +192,7 @@ fun TwoFaTokensScreen(
|
||||
@Composable
|
||||
private fun TwoFaTokenEditDialog(
|
||||
startValue: TwoFaTokenRecord?,
|
||||
isBusy: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (String, String, String, String?) -> Unit,
|
||||
) {
|
||||
@@ -235,14 +238,14 @@ private fun TwoFaTokenEditDialog(
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = issuer.isNotBlank() && account.isNotBlank() && secret.isNotBlank(),
|
||||
enabled = !isBusy && issuer.isNotBlank() && account.isNotBlank() && secret.isNotBlank(),
|
||||
onClick = { onSave(issuer, account, secret, notes.ifBlank { null }) },
|
||||
) {
|
||||
Text(stringResource(R.string.save))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
TextButton(onClick = onDismiss, enabled = !isBusy) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
|
||||
data class TwoFaTokensScreenState(
|
||||
val isLoading: Boolean = true,
|
||||
val isAvailable: Boolean = false,
|
||||
val isMutating: Boolean = false,
|
||||
val items: List<TwoFaTokenRecord> = emptyList(),
|
||||
val errorMessage: String? = null,
|
||||
)
|
||||
|
||||
@@ -3,12 +3,18 @@ package com.github.nullptroma.wallenc.ui.screens.main.screens.storage.twofa
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||
import com.github.nullptroma.wallenc.ui.screens.main.screens.storage.requireStorageUuid
|
||||
import com.github.nullptroma.wallenc.usecases.FindStorageUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.ManageTwoFaTokensUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -17,6 +23,8 @@ class TwoFaTokensViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val findStorageUseCase: FindStorageUseCase,
|
||||
private val manageTwoFaTokensUseCase: ManageTwoFaTokensUseCase,
|
||||
private val taskOrchestrator: ITaskOrchestrator,
|
||||
private val uiStrings: UiStringResolver,
|
||||
) : ViewModelBase<TwoFaTokensScreenState>(TwoFaTokensScreenState()) {
|
||||
|
||||
private val storageUuid = savedStateHandle.requireStorageUuid()
|
||||
@@ -40,10 +48,16 @@ class TwoFaTokensViewModel @Inject constructor(
|
||||
combine(
|
||||
storage.isAvailable,
|
||||
manageTwoFaTokensUseCase.observe(storage),
|
||||
) { available, items ->
|
||||
taskOrchestrator.pipelineState.map { pipe ->
|
||||
pipe.tasks.any { t ->
|
||||
t.busyStorageUuid == storage.uuid && isTaskActive(t.state)
|
||||
}
|
||||
},
|
||||
) { available, items, isMutating ->
|
||||
state.value.copy(
|
||||
isLoading = false,
|
||||
isAvailable = available,
|
||||
isMutating = isMutating,
|
||||
items = items,
|
||||
errorMessage = null,
|
||||
)
|
||||
@@ -62,6 +76,11 @@ class TwoFaTokensViewModel @Inject constructor(
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val storage = findStorageUseCase.find(storageUuid) ?: return@launch
|
||||
taskOrchestrator.enqueue(
|
||||
title = uiStrings(R.string.task_title_save_2fa_token),
|
||||
dispatcher = Dispatchers.IO,
|
||||
busyStorageUuid = storage.uuid,
|
||||
work = { _ ->
|
||||
if (existingId == null) {
|
||||
manageTwoFaTokensUseCase.create(
|
||||
storageInfo = storage,
|
||||
@@ -82,13 +101,25 @@ class TwoFaTokensViewModel @Inject constructor(
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteToken(id: String) {
|
||||
viewModelScope.launch {
|
||||
val storage = findStorageUseCase.find(storageUuid) ?: return@launch
|
||||
taskOrchestrator.enqueue(
|
||||
title = uiStrings(R.string.task_title_delete_2fa_token),
|
||||
dispatcher = Dispatchers.IO,
|
||||
busyStorageUuid = storage.uuid,
|
||||
work = { _ ->
|
||||
manageTwoFaTokensUseCase.delete(storage, id)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTaskActive(state: TaskRunState): Boolean =
|
||||
state is TaskRunState.Queued || state is TaskRunState.Running
|
||||
}
|
||||
|
||||
@@ -123,6 +123,10 @@
|
||||
<string name="task_title_remove_remote_vault">Удаление удалённого хранилища</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>
|
||||
<string name="task_title_delete_2fa_token">Удаление 2FA токена</string>
|
||||
<string name="task_title_save_text_secret">Сохранение текстового секрета</string>
|
||||
<string name="task_title_delete_text_secret">Удаление текстового секрета</string>
|
||||
|
||||
<string name="msg_encryption_enabled">Шифрование включено</string>
|
||||
<string name="msg_storage_already_encrypted">Хранилище уже зашифровано</string>
|
||||
|
||||
Reference in New Issue
Block a user