diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ProcessingScreen.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ProcessingScreen.kt new file mode 100644 index 0000000..61b2fa4 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ProcessingScreen.kt @@ -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, + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/TabState.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/TabState.kt index 95255fa..5d33b5b 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/TabState.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/TabState.kt @@ -77,6 +77,7 @@ class TabState @Inject constructor( title: String = "", subtitle: String = "", 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 isCancellable: Boolean = false, refreshEvenIfCrashes: Boolean = false, @@ -104,6 +105,12 @@ class TabState @Inject constructor( ) { callback(git) } + + if (positiveFeedbackText != null) { + launch { + errorsManager.emitPositiveNotification(positiveFeedbackText) + } + } } catch (ex: Exception) { hasProcessFailed = true ex.printStackTrace() diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/managers/ErrorsManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/managers/ErrorsManager.kt index a7a2d58..e078bb0 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/managers/ErrorsManager.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/managers/ErrorsManager.kt @@ -4,6 +4,7 @@ import com.jetpackduba.gitnuro.TaskType import com.jetpackduba.gitnuro.di.TabScope import com.jetpackduba.gitnuro.exceptions.GitnuroException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -20,6 +21,15 @@ class ErrorsManager @Inject constructor() { private val _error = MutableSharedFlow() val error: SharedFlow = _error + private val _notification = MutableStateFlow(null) + val notification: StateFlow = _notification + + suspend fun emitPositiveNotification(text: String) { + _notification.emit(text) + delay(2000) + _notification.emit(null) + } + suspend fun addError(error: Error) = withContext(Dispatchers.IO) { _errorsList.value = _errorsList.value.toMutableList().apply { add(error) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/AppTab.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/AppTab.kt index e634f92..de1d05e 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/AppTab.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/AppTab.kt @@ -1,25 +1,29 @@ package com.jetpackduba.gitnuro.ui -import androidx.compose.animation.Crossfade +import androidx.compose.animation.* 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.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment 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 com.jetpackduba.gitnuro.LoadingRepository +import com.jetpackduba.gitnuro.ProcessingScreen import com.jetpackduba.gitnuro.credentials.CredentialsAccepted import com.jetpackduba.gitnuro.credentials.CredentialsRequested import com.jetpackduba.gitnuro.credentials.CredentialsState import com.jetpackduba.gitnuro.git.ProcessingState -import com.jetpackduba.gitnuro.ui.components.PrimaryButton -import com.jetpackduba.gitnuro.ui.dialogs.* +import com.jetpackduba.gitnuro.ui.dialogs.CloneDialog +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.settings.SettingsDialog import com.jetpackduba.gitnuro.viewmodels.RepositorySelectionStatus @@ -32,11 +36,20 @@ fun AppTab( val errorManager = tabViewModel.errorsManager val lastError by errorManager.error.collectAsState(null) val showError by tabViewModel.showError.collectAsState() + val notification = errorManager.notification.collectAsState().value + var visibleNotification by remember { mutableStateOf("") } +// val (tabPosition, setTabPosition) = remember { mutableStateOf(null) } val repositorySelectionStatus = tabViewModel.repositorySelectionStatus.collectAsState() val repositorySelectionStatusValue = repositorySelectionStatus.value val processingState = tabViewModel.processing.collectAsState().value + LaunchedEffect(notification) { + if (notification != null) { + visibleNotification = notification + } + } + LaunchedEffect(tabViewModel) { // 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 @@ -107,53 +120,10 @@ fun AppTab( } if (processingState is ProcessingState.Processing) { - 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 = { tabViewModel.cancelOngoingTask() } - ) - } - } - } + ProcessingScreen( + processingState, + onCancelOnGoingTask = { tabViewModel.cancelOngoingTask() } + ) } @@ -164,6 +134,24 @@ fun AppTab( 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, + ) + } } } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/MenuViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/MenuViewModel.kt index 447eb59..f64c3cb 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/MenuViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/MenuViewModel.kt @@ -34,6 +34,7 @@ class MenuViewModel @Inject constructor( refreshType = RefreshType.ALL_DATA, title = "Pulling", subtitle = "Pulling changes from the remote branch to the current branch", + positiveFeedbackText = "Pull completed successfully", refreshEvenIfCrashes = true, taskType = TaskType.PULL, ) { git -> @@ -65,6 +66,7 @@ class MenuViewModel @Inject constructor( fun stash() = tabState.safeProcessing( refreshType = RefreshType.UNCOMMITTED_CHANGES_AND_LOG, taskType = TaskType.STASH, + positiveFeedbackText = "Changes have been stashed", ) { git -> stashChangesUseCase(git, null) }