Улучшение UI/UX

This commit is contained in:
2026-05-13 18:11:48 +03:00
parent abac9d93eb
commit c6df089668
7 changed files with 235 additions and 69 deletions

View File

@@ -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))
}
} }
} }

View File

@@ -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,

View File

@@ -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,
) )
} }

View File

@@ -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,

View File

@@ -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,16 +166,22 @@ 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",
) )
if (!started) {
updateState( updateState(
state.value.copy( state.value.copy(
userMessage = UserNotification.TextRes(R.string.sync_msg_task_enqueued), userMessage = UserNotification.TextRes(R.string.sync_msg_sync_already_running),
), ),
) )
} }
}
fun consumeUserMessage() {
updateState(state.value.copy(userMessage = null))
}
private fun observeVaults() { private fun observeVaults() {
viewModelScope.launch { viewModelScope.launch {
@@ -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()

View File

@@ -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>

View File

@@ -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,19 +16,27 @@ 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)) {
return false
}
_syncRunning.value = true
try {
val taskId = orchestrator.enqueue(
title = displayTitle, title = displayTitle,
dispatcher = Dispatchers.IO, dispatcher = Dispatchers.IO,
work = { ctx -> work = { ctx ->
if (!running.compareAndSet(false, true)) {
ctx.log(TaskLogLevel.Info, "Storage sync skipped (already running), reason=$logReason")
return@enqueue
}
try { try {
ctx.log(TaskLogLevel.Info, "Storage sync started, reason=$logReason") ctx.log(TaskLogLevel.Info, "Storage sync started, reason=$logReason")
ctx.reportProgress(null, "Storage sync: started") ctx.reportProgress(null, "Storage sync: started")
@@ -33,16 +45,24 @@ class RunStorageSyncUseCase(
} }
ctx.log(TaskLogLevel.Info, "Storage sync finished") ctx.log(TaskLogLevel.Info, "Storage sync finished")
ctx.reportProgress(null, "Storage sync: completed") ctx.reportProgress(null, "Storage sync: completed")
} } catch (e: Exception) {
catch (e: Exception) {
ctx.log(TaskLogLevel.Error, "Storage sync failed: ${e.message}") ctx.log(TaskLogLevel.Error, "Storage sync failed: ${e.message}")
ctx.reportProgress(null, "Storage sync: failed - ${e.message}") ctx.reportProgress(null, "Storage sync: failed - ${e.message}")
} } finally {
finally {
running.set(false) running.set(false)
_syncRunning.value = false
_activeSyncTaskId.value = null
} }
}, },
) )
_activeSyncTaskId.value = taskId
return true
} catch (t: Throwable) {
running.set(false)
_syncRunning.value = false
_activeSyncTaskId.value = null
throw t
}
} }
suspend fun runBlocking() { suspend fun runBlocking() {