Улучшение UI/UX
This commit is contained in:
@@ -67,17 +67,6 @@ fun TaskPipelineScreen(
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
Button(onClick = { showTestDialog = true }) {
|
|
||||||
Text(stringResource(R.string.task_pipeline_run_test))
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = { viewModel.orchestrator.cancelAll() },
|
|
||||||
enabled = hasAnyTask,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.task_pipeline_cancel_all))
|
|
||||||
}
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.task_pipeline_jobs),
|
stringResource(R.string.task_pipeline_jobs),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
@@ -113,6 +102,20 @@ fun TaskPipelineScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Button(
|
||||||
|
onClick = { showTestDialog = true },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.task_pipeline_run_test))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.orchestrator.cancelAll() },
|
||||||
|
enabled = hasAnyTask,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.task_pipeline_cancel_all))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,9 +143,18 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun notifyUser(@StringRes messageRes: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_userNotifications.emit(UserNotification.TextRes(messageRes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun printStorageInfoToLog(storage: IStorageInfo) {
|
fun printStorageInfoToLog(storage: IStorageInfo) {
|
||||||
val id = storage.uuid
|
val id = storage.uuid
|
||||||
if (isStorageTaskActive(id)) return
|
if (isStorageTaskActive(id)) {
|
||||||
|
notifyUser(R.string.vault_msg_storage_pipeline_busy)
|
||||||
|
return
|
||||||
|
}
|
||||||
taskOrchestrator.enqueue(
|
taskOrchestrator.enqueue(
|
||||||
title = uiStrings(R.string.task_title_dump_storage_log),
|
title = uiStrings(R.string.task_title_dump_storage_log),
|
||||||
dispatcher = Dispatchers.IO,
|
dispatcher = Dispatchers.IO,
|
||||||
@@ -182,6 +191,7 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
}
|
}
|
||||||
if (isVaultListMutationActive()) {
|
if (isVaultListMutationActive()) {
|
||||||
logger.debug(TAG, "createStorage ignored (vault list mutation already running)")
|
logger.debug(TAG, "createStorage ignored (vault list mutation already running)")
|
||||||
|
notifyUser(R.string.vault_msg_vault_list_mutation_busy)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.debug(TAG, "createStorage: enqueue task")
|
logger.debug(TAG, "createStorage: enqueue task")
|
||||||
@@ -209,7 +219,10 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
|
|
||||||
fun enableEncryption(storage: IStorageInfo, password: String, encryptPath: Boolean) {
|
fun enableEncryption(storage: IStorageInfo, password: String, encryptPath: Boolean) {
|
||||||
val id = storage.uuid
|
val id = storage.uuid
|
||||||
if (isStorageTaskActive(id)) return
|
if (isStorageTaskActive(id)) {
|
||||||
|
notifyUser(R.string.vault_msg_storage_pipeline_busy)
|
||||||
|
return
|
||||||
|
}
|
||||||
val key = EncryptKey(password)
|
val key = EncryptKey(password)
|
||||||
taskOrchestrator.enqueue(
|
taskOrchestrator.enqueue(
|
||||||
title = uiStrings(R.string.task_title_enable_encryption),
|
title = uiStrings(R.string.task_title_enable_encryption),
|
||||||
@@ -258,7 +271,10 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
|
|
||||||
fun openEncryptedStorage(storage: IStorageInfo, password: String, rememberPassword: Boolean) {
|
fun openEncryptedStorage(storage: IStorageInfo, password: String, rememberPassword: Boolean) {
|
||||||
val id = storage.uuid
|
val id = storage.uuid
|
||||||
if (isStorageTaskActive(id)) return
|
if (isStorageTaskActive(id)) {
|
||||||
|
notifyUser(R.string.vault_msg_storage_pipeline_busy)
|
||||||
|
return
|
||||||
|
}
|
||||||
val key = EncryptKey(password)
|
val key = EncryptKey(password)
|
||||||
taskOrchestrator.enqueue(
|
taskOrchestrator.enqueue(
|
||||||
title = uiStrings(R.string.task_title_open_encrypted_storage),
|
title = uiStrings(R.string.task_title_open_encrypted_storage),
|
||||||
@@ -285,7 +301,10 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
|
|
||||||
fun closeEncryptedStorage(storage: IStorageInfo) {
|
fun closeEncryptedStorage(storage: IStorageInfo) {
|
||||||
val id = storage.uuid
|
val id = storage.uuid
|
||||||
if (isStorageTaskActive(id)) return
|
if (isStorageTaskActive(id)) {
|
||||||
|
notifyUser(R.string.vault_msg_storage_pipeline_busy)
|
||||||
|
return
|
||||||
|
}
|
||||||
taskOrchestrator.enqueue(
|
taskOrchestrator.enqueue(
|
||||||
title = uiStrings(R.string.task_title_close_encrypted_storage),
|
title = uiStrings(R.string.task_title_close_encrypted_storage),
|
||||||
dispatcher = Dispatchers.IO,
|
dispatcher = Dispatchers.IO,
|
||||||
@@ -310,7 +329,10 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
|
|
||||||
fun disableEncryption(storage: IStorageInfo) {
|
fun disableEncryption(storage: IStorageInfo) {
|
||||||
val id = storage.uuid
|
val id = storage.uuid
|
||||||
if (isStorageTaskActive(id)) return
|
if (isStorageTaskActive(id)) {
|
||||||
|
notifyUser(R.string.vault_msg_storage_pipeline_busy)
|
||||||
|
return
|
||||||
|
}
|
||||||
taskOrchestrator.enqueue(
|
taskOrchestrator.enqueue(
|
||||||
title = uiStrings(R.string.task_title_disable_encryption),
|
title = uiStrings(R.string.task_title_disable_encryption),
|
||||||
dispatcher = Dispatchers.IO,
|
dispatcher = Dispatchers.IO,
|
||||||
@@ -338,7 +360,10 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
|
|
||||||
fun rename(storage: IStorageInfo, newName: String) {
|
fun rename(storage: IStorageInfo, newName: String) {
|
||||||
val id = storage.uuid
|
val id = storage.uuid
|
||||||
if (isStorageTaskActive(id)) return
|
if (isStorageTaskActive(id)) {
|
||||||
|
notifyUser(R.string.vault_msg_storage_pipeline_busy)
|
||||||
|
return
|
||||||
|
}
|
||||||
taskOrchestrator.enqueue(
|
taskOrchestrator.enqueue(
|
||||||
title = uiStrings(R.string.task_title_rename_storage),
|
title = uiStrings(R.string.task_title_rename_storage),
|
||||||
dispatcher = Dispatchers.IO,
|
dispatcher = Dispatchers.IO,
|
||||||
@@ -357,7 +382,10 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
|
|
||||||
fun remove(storage: IStorageInfo) {
|
fun remove(storage: IStorageInfo) {
|
||||||
val id = storage.uuid
|
val id = storage.uuid
|
||||||
if (isStorageTaskActive(id)) return
|
if (isStorageTaskActive(id)) {
|
||||||
|
notifyUser(R.string.vault_msg_storage_pipeline_busy)
|
||||||
|
return
|
||||||
|
}
|
||||||
taskOrchestrator.enqueue(
|
taskOrchestrator.enqueue(
|
||||||
title = uiStrings(R.string.task_title_remove_storage),
|
title = uiStrings(R.string.task_title_remove_storage),
|
||||||
dispatcher = Dispatchers.IO,
|
dispatcher = Dispatchers.IO,
|
||||||
@@ -399,7 +427,10 @@ abstract class AbstractVaultBrowserViewModel(
|
|||||||
|
|
||||||
fun clearStorageSyncLock(storage: IStorageInfo) {
|
fun clearStorageSyncLock(storage: IStorageInfo) {
|
||||||
val id = storage.uuid
|
val id = storage.uuid
|
||||||
if (isStorageTaskActive(id)) return
|
if (isStorageTaskActive(id)) {
|
||||||
|
notifyUser(R.string.vault_msg_storage_pipeline_busy)
|
||||||
|
return
|
||||||
|
}
|
||||||
taskOrchestrator.enqueue(
|
taskOrchestrator.enqueue(
|
||||||
title = uiStrings(R.string.task_title_clear_sync_lock),
|
title = uiStrings(R.string.task_title_clear_sync_lock),
|
||||||
dispatcher = Dispatchers.IO,
|
dispatcher = Dispatchers.IO,
|
||||||
|
|||||||
@@ -27,9 +27,12 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -37,6 +40,7 @@ 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.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -71,12 +75,33 @@ fun StorageSyncScreen(
|
|||||||
.flatMap { vault -> flattenStorageTree(vault.storages) }
|
.flatMap { vault -> flattenStorageTree(vault.storages) }
|
||||||
.associateBy { it.uuid }
|
.associateBy { it.uuid }
|
||||||
|
|
||||||
|
val groupEditLocked = state.isBusy || state.isStorageSyncRunning
|
||||||
|
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
val context = LocalContext.current
|
||||||
|
LaunchedEffect(state.userMessage) {
|
||||||
|
val um = state.userMessage ?: return@LaunchedEffect
|
||||||
|
val text = when (um) {
|
||||||
|
is UserNotification.TextRes -> {
|
||||||
|
if (um.formatArgs.isEmpty()) {
|
||||||
|
context.getString(um.id)
|
||||||
|
} else {
|
||||||
|
context.getString(um.id, *um.formatArgs.toTypedArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is UserNotification.Plain -> um.message
|
||||||
|
}
|
||||||
|
snackbarHostState.showSnackbar(text)
|
||||||
|
viewModel.consumeUserMessage()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = { if (!state.isBusy) viewModel.createGroup() },
|
onClick = { if (!groupEditLocked) viewModel.createGroup() },
|
||||||
modifier = Modifier.alpha(if (state.isBusy) 0.38f else 1f),
|
modifier = Modifier.alpha(if (groupEditLocked) 0.38f else 1f),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Add,
|
imageVector = Icons.Rounded.Add,
|
||||||
@@ -92,16 +117,13 @@ fun StorageSyncScreen(
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
if (state.isBusy) {
|
|
||||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
|
||||||
}
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = viewModel::runSyncNow,
|
onClick = viewModel::runSyncNow,
|
||||||
enabled = !state.isBusy,
|
enabled = !groupEditLocked && !state.anyVaultStoragesScanning,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Sync,
|
Icons.Rounded.Sync,
|
||||||
@@ -112,18 +134,46 @@ fun StorageSyncScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.userMessage?.let { um ->
|
when {
|
||||||
val text = when (um) {
|
state.isStorageSyncRunning -> {
|
||||||
is UserNotification.TextRes -> {
|
Column(
|
||||||
if (um.formatArgs.isEmpty()) {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
stringResource(um.id)
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.sync_progress_section_title),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
state.storageSyncProgressLabel?.let { line ->
|
||||||
|
Text(
|
||||||
|
text = line,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val frac = state.storageSyncProgressFraction
|
||||||
|
if (frac != null) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { frac },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
stringResource(um.id, *um.formatArgs.toTypedArray())
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is UserNotification.Plain -> um.message
|
|
||||||
}
|
}
|
||||||
Text(text = text, style = MaterialTheme.typography.bodyMedium)
|
state.isBusy -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.sync_groups_busy_section_title),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -164,7 +214,7 @@ fun StorageSyncScreen(
|
|||||||
)
|
)
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { pendingRemoveGroupId = group.id },
|
onClick = { pendingRemoveGroupId = group.id },
|
||||||
enabled = !state.isBusy,
|
enabled = !groupEditLocked,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Delete,
|
imageVector = Icons.Rounded.Delete,
|
||||||
@@ -254,7 +304,7 @@ fun StorageSyncScreen(
|
|||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { pendingRemoveStorage = group.id to storageUuid },
|
onClick = { pendingRemoveStorage = group.id to storageUuid },
|
||||||
enabled = !state.isBusy,
|
enabled = !groupEditLocked,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Delete,
|
imageVector = Icons.Rounded.Delete,
|
||||||
@@ -274,7 +324,7 @@ fun StorageSyncScreen(
|
|||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { viewModel.openPicker(group.id) },
|
onClick = { viewModel.openPicker(group.id) },
|
||||||
enabled = !state.isBusy,
|
enabled = !groupEditLocked,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Add,
|
imageVector = Icons.Rounded.Add,
|
||||||
@@ -376,6 +426,7 @@ private fun StoragePickerScreen(
|
|||||||
onToggleVault: (UUID) -> Unit,
|
onToggleVault: (UUID) -> Unit,
|
||||||
) {
|
) {
|
||||||
val selected = state.groups.firstOrNull { it.id == groupId }?.storageUuids ?: emptySet()
|
val selected = state.groups.firstOrNull { it.id == groupId }?.storageUuids ?: emptySet()
|
||||||
|
val groupEditLocked = state.isBusy || state.isStorageSyncRunning
|
||||||
Scaffold(modifier = modifier) { inner ->
|
Scaffold(modifier = modifier) { inner ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -465,7 +516,7 @@ private fun StoragePickerScreen(
|
|||||||
node = storage,
|
node = storage,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
selected = selected,
|
selected = selected,
|
||||||
isBusy = state.isBusy,
|
isBusy = groupEditLocked,
|
||||||
onAddStorage = onAddStorage,
|
onAddStorage = onAddStorage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ data class StorageSyncScreenState(
|
|||||||
val expandedVaultUuids: Set<UUID> = emptySet(),
|
val expandedVaultUuids: Set<UUID> = emptySet(),
|
||||||
val pickerGroupId: String? = null,
|
val pickerGroupId: String? = null,
|
||||||
val isBusy: Boolean = false,
|
val isBusy: Boolean = false,
|
||||||
|
/** Долгая синхронизация storages через пайплайн задач. */
|
||||||
|
val isStorageSyncRunning: Boolean = false,
|
||||||
|
/** Доля прогресса активной синхронизации; null — неопределённый прогресс. */
|
||||||
|
val storageSyncProgressFraction: Float? = null,
|
||||||
|
/** Подпись этапа из пайплайна (если есть). */
|
||||||
|
val storageSyncProgressLabel: String? = null,
|
||||||
/** Любой vault ещё загружает список storages — UUID из группы могут появиться позже. */
|
/** Любой vault ещё загружает список storages — UUID из группы могут появиться позже. */
|
||||||
val anyVaultStoragesScanning: Boolean = false,
|
val anyVaultStoragesScanning: Boolean = false,
|
||||||
val userMessage: UserNotification? = null,
|
val userMessage: UserNotification? = null,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
||||||
|
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.R
|
||||||
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
import com.github.nullptroma.wallenc.ui.ViewModelBase
|
||||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||||
@@ -28,12 +30,14 @@ class StorageSyncViewModel @Inject constructor(
|
|||||||
private val groupsUseCase: ManageStorageSyncGroupsUseCase,
|
private val groupsUseCase: ManageStorageSyncGroupsUseCase,
|
||||||
private val runStorageSyncUseCase: RunStorageSyncUseCase,
|
private val runStorageSyncUseCase: RunStorageSyncUseCase,
|
||||||
private val vaultsManager: IVaultsManager,
|
private val vaultsManager: IVaultsManager,
|
||||||
|
private val taskOrchestrator: ITaskOrchestrator,
|
||||||
private val uiStrings: UiStringResolver,
|
private val uiStrings: UiStringResolver,
|
||||||
) : ViewModelBase<StorageSyncScreenState>(StorageSyncScreenState()) {
|
) : ViewModelBase<StorageSyncScreenState>(StorageSyncScreenState()) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
refreshGroups()
|
refreshGroups()
|
||||||
observeVaults()
|
observeVaults()
|
||||||
|
observeStorageSyncPipeline()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
vaultsManager.vaults
|
vaultsManager.vaults
|
||||||
.flatMapLatest { vaults ->
|
.flatMapLatest { vaults ->
|
||||||
@@ -52,6 +56,36 @@ class StorageSyncViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeStorageSyncPipeline() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
combine(
|
||||||
|
runStorageSyncUseCase.syncRunning,
|
||||||
|
runStorageSyncUseCase.activeSyncTaskId,
|
||||||
|
taskOrchestrator.pipelineState,
|
||||||
|
) { syncRunning, taskId, pipe ->
|
||||||
|
val progress = taskId?.let { id ->
|
||||||
|
val task = pipe.tasks.find { it.id == id }
|
||||||
|
(task?.state as? TaskRunState.Running)?.progress
|
||||||
|
}
|
||||||
|
Triple(
|
||||||
|
syncRunning,
|
||||||
|
progress?.fraction,
|
||||||
|
progress?.label?.takeIf { it.isNotBlank() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect { (running, frac, label) ->
|
||||||
|
updateState(
|
||||||
|
state.value.copy(
|
||||||
|
isStorageSyncRunning = running,
|
||||||
|
storageSyncProgressFraction = if (running) frac else null,
|
||||||
|
storageSyncProgressLabel = if (running) label else null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun refreshGroups() {
|
fun refreshGroups() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
updateState(state.value.copy(groups = reloadGroupsUi()))
|
updateState(state.value.copy(groups = reloadGroupsUi()))
|
||||||
@@ -80,6 +114,7 @@ class StorageSyncViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun openPicker(groupId: String) {
|
fun openPicker(groupId: String) {
|
||||||
|
if (state.value.isBusy || state.value.isStorageSyncRunning) return
|
||||||
updateState(
|
updateState(
|
||||||
state.value.copy(
|
state.value.copy(
|
||||||
pickerGroupId = groupId,
|
pickerGroupId = groupId,
|
||||||
@@ -131,15 +166,21 @@ class StorageSyncViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun runSyncNow() {
|
fun runSyncNow() {
|
||||||
runStorageSyncUseCase.enqueue(
|
val started = runStorageSyncUseCase.enqueue(
|
||||||
displayTitle = uiStrings(R.string.task_title_storage_sync),
|
displayTitle = uiStrings(R.string.task_title_storage_sync),
|
||||||
logReason = "sync-tab",
|
logReason = "sync-tab",
|
||||||
)
|
)
|
||||||
updateState(
|
if (!started) {
|
||||||
state.value.copy(
|
updateState(
|
||||||
userMessage = UserNotification.TextRes(R.string.sync_msg_task_enqueued),
|
state.value.copy(
|
||||||
),
|
userMessage = UserNotification.TextRes(R.string.sync_msg_sync_already_running),
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consumeUserMessage() {
|
||||||
|
updateState(state.value.copy(userMessage = null))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeVaults() {
|
private fun observeVaults() {
|
||||||
@@ -297,6 +338,14 @@ class StorageSyncViewModel @Inject constructor(
|
|||||||
clearPicker: Boolean = false,
|
clearPicker: Boolean = false,
|
||||||
block: suspend () -> UserNotification?,
|
block: suspend () -> UserNotification?,
|
||||||
) {
|
) {
|
||||||
|
if (state.value.isStorageSyncRunning) {
|
||||||
|
updateState(
|
||||||
|
state.value.copy(
|
||||||
|
userMessage = UserNotification.TextRes(R.string.sync_msg_blocked_during_sync),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
updateState(state.value.copy(isBusy = true, userMessage = null))
|
updateState(state.value.copy(isBusy = true, userMessage = null))
|
||||||
val message = block()
|
val message = block()
|
||||||
val groups = reloadGroupsUi()
|
val groups = reloadGroupsUi()
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
<string name="settings_title">Настройки</string>
|
<string name="settings_title">Настройки</string>
|
||||||
<string name="sync_groups_title">Группы синхронизации</string>
|
<string name="sync_groups_title">Группы синхронизации</string>
|
||||||
|
<string name="sync_progress_section_title">Синхронизация хранилищ</string>
|
||||||
|
<string name="sync_groups_busy_section_title">Сохранение групп синхронизации</string>
|
||||||
<string name="sync_run_now">Запустить синхронизацию</string>
|
<string name="sync_run_now">Запустить синхронизацию</string>
|
||||||
<string name="sync_cd_run_now">Запустить синхронизацию сейчас</string>
|
<string name="sync_cd_run_now">Запустить синхронизацию сейчас</string>
|
||||||
<string name="sync_refresh">Обновить</string>
|
<string name="sync_refresh">Обновить</string>
|
||||||
@@ -43,6 +45,8 @@
|
|||||||
<string name="sync_msg_storage_added">Хранилище добавлено в %1$s</string>
|
<string name="sync_msg_storage_added">Хранилище добавлено в %1$s</string>
|
||||||
<string name="sync_msg_storage_removed">Хранилище убрано из %1$s</string>
|
<string name="sync_msg_storage_removed">Хранилище убрано из %1$s</string>
|
||||||
<string name="sync_msg_task_enqueued">Задача синхронизации поставлена в очередь</string>
|
<string name="sync_msg_task_enqueued">Задача синхронизации поставлена в очередь</string>
|
||||||
|
<string name="sync_msg_sync_already_running">Синхронизация уже выполняется</string>
|
||||||
|
<string name="sync_msg_blocked_during_sync">Дождитесь окончания синхронизации</string>
|
||||||
<string name="sync_encryption_unknown">Неизвестно</string>
|
<string name="sync_encryption_unknown">Неизвестно</string>
|
||||||
<string name="sync_storage_encryption_line">Шифрование: %1$s</string>
|
<string name="sync_storage_encryption_line">Шифрование: %1$s</string>
|
||||||
<string name="sync_storage_missing_title">Не найдено в текущих vault</string>
|
<string name="sync_storage_missing_title">Не найдено в текущих vault</string>
|
||||||
@@ -80,6 +84,8 @@
|
|||||||
<string name="vault_fab_add_storage_cd">Создать хранилище</string>
|
<string name="vault_fab_add_storage_cd">Создать хранилище</string>
|
||||||
<string name="vault_fab_add_storage_disabled_cd">Создание недоступно: хранилище недоступно</string>
|
<string name="vault_fab_add_storage_disabled_cd">Создание недоступно: хранилище недоступно</string>
|
||||||
<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_vault_list_mutation_busy">Список хранилищ сейчас меняется — подождите</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>
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ package com.github.nullptroma.wallenc.usecases
|
|||||||
|
|
||||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncEngine
|
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncEngine
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||||
|
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
||||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class RunStorageSyncUseCase(
|
class RunStorageSyncUseCase(
|
||||||
@@ -12,37 +16,53 @@ class RunStorageSyncUseCase(
|
|||||||
) {
|
) {
|
||||||
private val running = AtomicBoolean(false)
|
private val running = AtomicBoolean(false)
|
||||||
|
|
||||||
|
private val _syncRunning = MutableStateFlow(false)
|
||||||
|
val syncRunning: StateFlow<Boolean> = _syncRunning.asStateFlow()
|
||||||
|
|
||||||
|
private val _activeSyncTaskId = MutableStateFlow<TaskId?>(null)
|
||||||
|
val activeSyncTaskId: StateFlow<TaskId?> = _activeSyncTaskId.asStateFlow()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param displayTitle заголовок задачи в UI (локализованный на стороне вызова)
|
* @param displayTitle заголовок задачи в UI (локализованный на стороне вызова)
|
||||||
* @param logReason техническая метка для логов (не для UI)
|
* @param logReason техническая метка для логов (не для UI)
|
||||||
|
* @return false, если синхронизация уже в очереди или выполняется — новая задача не создана
|
||||||
*/
|
*/
|
||||||
fun enqueue(displayTitle: String, logReason: String) {
|
fun enqueue(displayTitle: String, logReason: String): Boolean {
|
||||||
orchestrator.enqueue(
|
if (!running.compareAndSet(false, true)) {
|
||||||
title = displayTitle,
|
return false
|
||||||
dispatcher = Dispatchers.IO,
|
}
|
||||||
work = { ctx ->
|
_syncRunning.value = true
|
||||||
if (!running.compareAndSet(false, true)) {
|
try {
|
||||||
ctx.log(TaskLogLevel.Info, "Storage sync skipped (already running), reason=$logReason")
|
val taskId = orchestrator.enqueue(
|
||||||
return@enqueue
|
title = displayTitle,
|
||||||
}
|
dispatcher = Dispatchers.IO,
|
||||||
try {
|
work = { ctx ->
|
||||||
ctx.log(TaskLogLevel.Info, "Storage sync started, reason=$logReason")
|
try {
|
||||||
ctx.reportProgress(null, "Storage sync: started")
|
ctx.log(TaskLogLevel.Info, "Storage sync started, reason=$logReason")
|
||||||
syncEngine.syncAllGroups { fraction, label ->
|
ctx.reportProgress(null, "Storage sync: started")
|
||||||
ctx.reportProgress(fraction, label)
|
syncEngine.syncAllGroups { fraction, label ->
|
||||||
|
ctx.reportProgress(fraction, label)
|
||||||
|
}
|
||||||
|
ctx.log(TaskLogLevel.Info, "Storage sync finished")
|
||||||
|
ctx.reportProgress(null, "Storage sync: completed")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ctx.log(TaskLogLevel.Error, "Storage sync failed: ${e.message}")
|
||||||
|
ctx.reportProgress(null, "Storage sync: failed - ${e.message}")
|
||||||
|
} finally {
|
||||||
|
running.set(false)
|
||||||
|
_syncRunning.value = false
|
||||||
|
_activeSyncTaskId.value = null
|
||||||
}
|
}
|
||||||
ctx.log(TaskLogLevel.Info, "Storage sync finished")
|
},
|
||||||
ctx.reportProgress(null, "Storage sync: completed")
|
)
|
||||||
}
|
_activeSyncTaskId.value = taskId
|
||||||
catch (e: Exception) {
|
return true
|
||||||
ctx.log(TaskLogLevel.Error, "Storage sync failed: ${e.message}")
|
} catch (t: Throwable) {
|
||||||
ctx.reportProgress(null, "Storage sync: failed - ${e.message}")
|
running.set(false)
|
||||||
}
|
_syncRunning.value = false
|
||||||
finally {
|
_activeSyncTaskId.value = null
|
||||||
running.set(false)
|
throw t
|
||||||
}
|
}
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun runBlocking() {
|
suspend fun runBlocking() {
|
||||||
|
|||||||
Reference in New Issue
Block a user