From 2e825be44b8f6c03efb6fafdbe180126bf6750fe Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Sun, 30 Apr 2023 15:45:41 +0200 Subject: [PATCH] Fixed UX issues with clone dialog --- .../gitnuro/ui/dialogs/CloneDialog.kt | 48 ++++++++++--------- .../gitnuro/viewmodels/CloneViewModel.kt | 44 ++++++++++------- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/CloneDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/CloneDialog.kt index 4c3c6e2..c8955f2 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/CloneDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/CloneDialog.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import com.jetpackduba.gitnuro.extensions.handMouseClickable import com.jetpackduba.gitnuro.git.CloneState @@ -63,13 +65,7 @@ fun CloneDialog( onClose() } - is CloneState.Fail -> CloneInput( - cloneViewModel = cloneViewModel, - onClose = onClose, - errorMessage = cloneStatusValue.reason - ) - - CloneState.None -> CloneInput( + is CloneState.Fail, CloneState.None -> CloneDialogView( cloneViewModel = cloneViewModel, onClose = onClose, ) @@ -79,15 +75,16 @@ fun CloneDialog( } @Composable -private fun CloneInput( +private fun CloneDialogView( cloneViewModel: CloneViewModel, onClose: () -> Unit, - errorMessage: String? = null, ) { - var url by remember { mutableStateOf(cloneViewModel.url) } - var directory by remember { mutableStateOf(cloneViewModel.directory) } + var url by remember(cloneViewModel) { mutableStateOf(cloneViewModel.repositoryUrl.value) } + var directory by remember(cloneViewModel) { mutableStateOf(cloneViewModel.directoryPath.value) } var cloneSubmodules by remember { mutableStateOf(true) } + val error by cloneViewModel.error.collectAsState() + val urlFocusRequester = remember { FocusRequester() } val directoryFocusRequester = remember { FocusRequester() } val directoryButtonFocusRequester = remember { FocusRequester() } @@ -115,10 +112,10 @@ private fun CloneInput( previous = cancelButtonFocusRequester next = directoryFocusRequester }, - onValueChange = { + onValueChange = { repositoryUrl -> + url = repositoryUrl + cloneViewModel.onRepositoryUrlChanged(repositoryUrl) cloneViewModel.resetStateIfError() - url = it - cloneViewModel.url = url } ) @@ -138,9 +135,9 @@ private fun CloneInput( next = directoryButtonFocusRequester }, onValueChange = { - cloneViewModel.resetStateIfError() directory = it - cloneViewModel.directory = directory + cloneViewModel.onDirectoryPathChanged(directory) + cloneViewModel.resetStateIfError() }, ) @@ -149,8 +146,9 @@ private fun CloneInput( cloneViewModel.resetStateIfError() val newDirectory = cloneViewModel.openDirectoryPicker() if (newDirectory != null) { - directory = newDirectory - cloneViewModel.directory = directory + directory = TextFieldValue(newDirectory, selection = TextRange(newDirectory.count())) + cloneViewModel.onDirectoryPathChanged(directory) + cloneViewModel.resetStateIfError() } }, modifier = Modifier @@ -200,7 +198,7 @@ private fun CloneInput( ) } - AnimatedVisibility (errorMessage != null) { + AnimatedVisibility (error.isNotBlank()) { Box( modifier = Modifier .fillMaxWidth() @@ -209,7 +207,7 @@ private fun CloneInput( .background(MaterialTheme.colors.error) ) { Text( - errorMessage.orEmpty(), + error, modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp, horizontal = 8.dp), @@ -237,7 +235,7 @@ private fun CloneInput( ) PrimaryButton( onClick = { - cloneViewModel.clone(directory, url, cloneSubmodules) + cloneViewModel.clone(directory.text, url.text, cloneSubmodules) }, modifier = Modifier .focusRequester(cloneButtonFocusRequester) @@ -248,6 +246,10 @@ private fun CloneInput( text = "Clone" ) } + + LaunchedEffect(Unit) { + urlFocusRequester.requestFocus() + } } } @@ -333,11 +335,11 @@ private fun Cancelling() { private fun TextInput( modifier: Modifier = Modifier, title: String, - value: String, + value: TextFieldValue, enabled: Boolean = true, focusRequester: FocusRequester, focusProperties: FocusProperties.() -> Unit, - onValueChange: (String) -> Unit, + onValueChange: (TextFieldValue) -> Unit, textFieldShape: Shape = RoundedCornerShape(4.dp), ) { Column( diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/CloneViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/CloneViewModel.kt index 6386051..eb0cb26 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/CloneViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/CloneViewModel.kt @@ -1,5 +1,6 @@ package com.jetpackduba.gitnuro.viewmodels +import androidx.compose.ui.text.input.TextFieldValue import com.jetpackduba.gitnuro.git.CloneState import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.remote_operations.CloneRepositoryUseCase @@ -9,7 +10,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flowOn import java.io.File import javax.inject.Inject @@ -19,25 +20,29 @@ class CloneViewModel @Inject constructor( private val cloneRepositoryUseCase: CloneRepositoryUseCase, private val openFilePickerUseCase: OpenFilePickerUseCase, ) { + private val _repositoryUrl = MutableStateFlow(TextFieldValue("")) + val repositoryUrl = _repositoryUrl.asStateFlow() + + private val _directoryPath = MutableStateFlow(TextFieldValue("")) + val directoryPath = _directoryPath.asStateFlow() private val _cloneState = MutableStateFlow(CloneState.None) - val cloneState: StateFlow - get() = _cloneState + val cloneState = _cloneState.asStateFlow() - var url: String = "" - var directory: String = "" + private val _error = MutableStateFlow("") + val error = _error.asStateFlow() private var cloneJob: Job? = null fun clone(directoryPath: String, url: String, cloneSubmodules: Boolean) { cloneJob = tabState.safeProcessingWithoutGit { if (directoryPath.isBlank()) { - _cloneState.value = CloneState.Fail("Invalid empty directory") + _error.value = "Invalid empty directory" return@safeProcessingWithoutGit } if (url.isBlank()) { - _cloneState.value = CloneState.Fail("Invalid empty URL") + _error.value = "Invalid empty URL" return@safeProcessingWithoutGit } @@ -57,7 +62,7 @@ class CloneViewModel @Inject constructor( } if (repoName.isNullOrBlank()) { - _cloneState.value = CloneState.Fail("Check your URL and try again") + _error.value = "Check your URL and try again" return@safeProcessingWithoutGit } @@ -75,17 +80,16 @@ class CloneViewModel @Inject constructor( cloneRepositoryUseCase(repoDir, url, cloneSubmodules) .flowOn(Dispatchers.IO) .collect { newCloneStatus -> - _cloneState.value = newCloneStatus + if (newCloneStatus is CloneState.Fail) { + _error.value = newCloneStatus.reason + _cloneState.value = CloneState.None + } else { + _cloneState.value = newCloneStatus + } } } } - fun reset() { - _cloneState.value = CloneState.None - url = "" - directory = "" - } - fun cancelClone() = tabState.safeProcessingWithoutGit { _cloneState.value = CloneState.Cancelling cloneJob?.cancelAndJoin() @@ -93,10 +97,18 @@ class CloneViewModel @Inject constructor( } fun resetStateIfError() { - _cloneState.value = CloneState.None + _error.value = "" } fun openDirectoryPicker(): String? { return openFilePickerUseCase(PickerType.DIRECTORIES, null) } + + fun onDirectoryPathChanged(directory: TextFieldValue) { + _directoryPath.value = directory + } + + fun onRepositoryUrlChanged(repositoryUrl: TextFieldValue) { + _repositoryUrl.value = repositoryUrl + } } \ No newline at end of file