Added basic positive feedback

This commit is contained in:
Abdelilah El Aissaoui 2024-06-23 14:56:39 +02:00
parent 8ca2c0be66
commit 2375f43a44
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
5 changed files with 133 additions and 55 deletions

View File

@ -0,0 +1,71 @@
package com.jetpackduba.gitnuro
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.git.ProcessingState
import com.jetpackduba.gitnuro.ui.components.PrimaryButton
@Composable
fun ProcessingScreen(
processingState: ProcessingState.Processing,
onCancelOnGoingTask: () -> Unit,
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.surface)
.onPreviewKeyEvent { true } // Disable all keyboard events
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {},
),
contentAlignment = Alignment.Center,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (processingState.title.isNotEmpty()) {
Text(
processingState.title,
style = MaterialTheme.typography.h3,
color = MaterialTheme.colors.onBackground,
modifier = Modifier.padding(bottom = 8.dp),
)
}
if (processingState.subtitle.isNotEmpty()) {
Text(
processingState.subtitle,
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground,
modifier = Modifier.padding(bottom = 32.dp),
)
}
LinearProgressIndicator(
modifier = Modifier.width(280.dp)
.padding(bottom = 32.dp),
color = MaterialTheme.colors.secondary,
)
if (processingState.isCancellable) {
PrimaryButton(
text = "Cancel",
onClick = onCancelOnGoingTask,
)
}
}
}
}

View File

@ -77,6 +77,7 @@ class TabState @Inject constructor(
title: String = "", title: String = "",
subtitle: String = "", subtitle: String = "",
taskType: TaskType, taskType: TaskType,
positiveFeedbackText: String? = null,
// TODO For now have it always as false because the data refresh is cancelled even when the git process couldn't be cancelled // TODO For now have it always as false because the data refresh is cancelled even when the git process couldn't be cancelled
isCancellable: Boolean = false, isCancellable: Boolean = false,
refreshEvenIfCrashes: Boolean = false, refreshEvenIfCrashes: Boolean = false,
@ -104,6 +105,12 @@ class TabState @Inject constructor(
) { ) {
callback(git) callback(git)
} }
if (positiveFeedbackText != null) {
launch {
errorsManager.emitPositiveNotification(positiveFeedbackText)
}
}
} catch (ex: Exception) { } catch (ex: Exception) {
hasProcessFailed = true hasProcessFailed = true
ex.printStackTrace() ex.printStackTrace()

View File

@ -4,6 +4,7 @@ import com.jetpackduba.gitnuro.TaskType
import com.jetpackduba.gitnuro.di.TabScope import com.jetpackduba.gitnuro.di.TabScope
import com.jetpackduba.gitnuro.exceptions.GitnuroException import com.jetpackduba.gitnuro.exceptions.GitnuroException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
@ -20,6 +21,15 @@ class ErrorsManager @Inject constructor() {
private val _error = MutableSharedFlow<Error?>() private val _error = MutableSharedFlow<Error?>()
val error: SharedFlow<Error?> = _error val error: SharedFlow<Error?> = _error
private val _notification = MutableStateFlow<String?>(null)
val notification: StateFlow<String?> = _notification
suspend fun emitPositiveNotification(text: String) {
_notification.emit(text)
delay(2000)
_notification.emit(null)
}
suspend fun addError(error: Error) = withContext(Dispatchers.IO) { suspend fun addError(error: Error) = withContext(Dispatchers.IO) {
_errorsList.value = _errorsList.value.toMutableList().apply { _errorsList.value = _errorsList.value.toMutableList().apply {
add(error) add(error)

View File

@ -1,25 +1,29 @@
package com.jetpackduba.gitnuro.ui package com.jetpackduba.gitnuro.ui
import androidx.compose.animation.Crossfade import androidx.compose.animation.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.LinearProgressIndicator import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.LoadingRepository import com.jetpackduba.gitnuro.LoadingRepository
import com.jetpackduba.gitnuro.ProcessingScreen
import com.jetpackduba.gitnuro.credentials.CredentialsAccepted import com.jetpackduba.gitnuro.credentials.CredentialsAccepted
import com.jetpackduba.gitnuro.credentials.CredentialsRequested import com.jetpackduba.gitnuro.credentials.CredentialsRequested
import com.jetpackduba.gitnuro.credentials.CredentialsState import com.jetpackduba.gitnuro.credentials.CredentialsState
import com.jetpackduba.gitnuro.git.ProcessingState import com.jetpackduba.gitnuro.git.ProcessingState
import com.jetpackduba.gitnuro.ui.components.PrimaryButton import com.jetpackduba.gitnuro.ui.dialogs.CloneDialog
import com.jetpackduba.gitnuro.ui.dialogs.* import com.jetpackduba.gitnuro.ui.dialogs.GpgPasswordDialog
import com.jetpackduba.gitnuro.ui.dialogs.SshPasswordDialog
import com.jetpackduba.gitnuro.ui.dialogs.UserPasswordDialog
import com.jetpackduba.gitnuro.ui.dialogs.errors.ErrorDialog import com.jetpackduba.gitnuro.ui.dialogs.errors.ErrorDialog
import com.jetpackduba.gitnuro.ui.dialogs.settings.SettingsDialog import com.jetpackduba.gitnuro.ui.dialogs.settings.SettingsDialog
import com.jetpackduba.gitnuro.viewmodels.RepositorySelectionStatus import com.jetpackduba.gitnuro.viewmodels.RepositorySelectionStatus
@ -32,11 +36,20 @@ fun AppTab(
val errorManager = tabViewModel.errorsManager val errorManager = tabViewModel.errorsManager
val lastError by errorManager.error.collectAsState(null) val lastError by errorManager.error.collectAsState(null)
val showError by tabViewModel.showError.collectAsState() val showError by tabViewModel.showError.collectAsState()
val notification = errorManager.notification.collectAsState().value
var visibleNotification by remember { mutableStateOf("") }
// val (tabPosition, setTabPosition) = remember { mutableStateOf<LayoutCoordinates?>(null) }
val repositorySelectionStatus = tabViewModel.repositorySelectionStatus.collectAsState() val repositorySelectionStatus = tabViewModel.repositorySelectionStatus.collectAsState()
val repositorySelectionStatusValue = repositorySelectionStatus.value val repositorySelectionStatusValue = repositorySelectionStatus.value
val processingState = tabViewModel.processing.collectAsState().value val processingState = tabViewModel.processing.collectAsState().value
LaunchedEffect(notification) {
if (notification != null) {
visibleNotification = notification
}
}
LaunchedEffect(tabViewModel) { LaunchedEffect(tabViewModel) {
// Init the tab content when the tab is selected and also remove the "initialPath" to avoid opening the // Init the tab content when the tab is selected and also remove the "initialPath" to avoid opening the
// repository everytime the user changes between tabs // repository everytime the user changes between tabs
@ -107,55 +120,12 @@ fun AppTab(
} }
if (processingState is ProcessingState.Processing) { if (processingState is ProcessingState.Processing) {
Box( ProcessingScreen(
modifier = Modifier processingState,
.fillMaxSize() onCancelOnGoingTask = { tabViewModel.cancelOngoingTask() }
.background(MaterialTheme.colors.surface)
.onPreviewKeyEvent { true } // Disable all keyboard events
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {},
),
contentAlignment = Alignment.Center,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (processingState.title.isNotEmpty()) {
Text(
processingState.title,
style = MaterialTheme.typography.h3,
color = MaterialTheme.colors.onBackground,
modifier = Modifier.padding(bottom = 8.dp),
) )
} }
if (processingState.subtitle.isNotEmpty()) {
Text(
processingState.subtitle,
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground,
modifier = Modifier.padding(bottom = 32.dp),
)
}
LinearProgressIndicator(
modifier = Modifier.width(280.dp)
.padding(bottom = 32.dp),
color = MaterialTheme.colors.secondary,
)
if (processingState.isCancellable) {
PrimaryButton(
text = "Cancel",
onClick = { tabViewModel.cancelOngoingTask() }
)
}
}
}
}
val safeLastError = lastError val safeLastError = lastError
if (safeLastError != null && showError) { if (safeLastError != null && showError) {
@ -164,6 +134,24 @@ fun AppTab(
onAccept = { tabViewModel.showError.value = false } onAccept = { tabViewModel.showError.value = false }
) )
} }
AnimatedVisibility(
visible = notification != null,
modifier = Modifier.align(Alignment.BottomCenter),
enter = fadeIn() + slideInVertically { it * 2 },
exit = fadeOut() + slideOutVertically { it * 2 },
) {
Text(
text = visibleNotification,
modifier = Modifier
.padding(bottom = 48.dp)
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colors.primary)
.padding(8.dp),
color = MaterialTheme.colors.onPrimary,
style = MaterialTheme.typography.body1,
)
}
} }
} }

View File

@ -34,6 +34,7 @@ class MenuViewModel @Inject constructor(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
title = "Pulling", title = "Pulling",
subtitle = "Pulling changes from the remote branch to the current branch", subtitle = "Pulling changes from the remote branch to the current branch",
positiveFeedbackText = "Pull completed successfully",
refreshEvenIfCrashes = true, refreshEvenIfCrashes = true,
taskType = TaskType.PULL, taskType = TaskType.PULL,
) { git -> ) { git ->
@ -65,6 +66,7 @@ class MenuViewModel @Inject constructor(
fun stash() = tabState.safeProcessing( fun stash() = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITTED_CHANGES_AND_LOG, refreshType = RefreshType.UNCOMMITTED_CHANGES_AND_LOG,
taskType = TaskType.STASH, taskType = TaskType.STASH,
positiveFeedbackText = "Changes have been stashed",
) { git -> ) { git ->
stashChangesUseCase(git, null) stashChangesUseCase(git, null)
} }