Fixed UX issues with clone dialog

This commit is contained in:
Abdelilah El Aissaoui 2023-04-30 15:45:41 +02:00
parent 73816089a6
commit 2e825be44b
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
2 changed files with 53 additions and 39 deletions

View File

@ -19,6 +19,8 @@ import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape 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 androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.extensions.handMouseClickable import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.git.CloneState import com.jetpackduba.gitnuro.git.CloneState
@ -63,13 +65,7 @@ fun CloneDialog(
onClose() onClose()
} }
is CloneState.Fail -> CloneInput( is CloneState.Fail, CloneState.None -> CloneDialogView(
cloneViewModel = cloneViewModel,
onClose = onClose,
errorMessage = cloneStatusValue.reason
)
CloneState.None -> CloneInput(
cloneViewModel = cloneViewModel, cloneViewModel = cloneViewModel,
onClose = onClose, onClose = onClose,
) )
@ -79,15 +75,16 @@ fun CloneDialog(
} }
@Composable @Composable
private fun CloneInput( private fun CloneDialogView(
cloneViewModel: CloneViewModel, cloneViewModel: CloneViewModel,
onClose: () -> Unit, onClose: () -> Unit,
errorMessage: String? = null,
) { ) {
var url by remember { mutableStateOf(cloneViewModel.url) } var url by remember(cloneViewModel) { mutableStateOf(cloneViewModel.repositoryUrl.value) }
var directory by remember { mutableStateOf(cloneViewModel.directory) } var directory by remember(cloneViewModel) { mutableStateOf(cloneViewModel.directoryPath.value) }
var cloneSubmodules by remember { mutableStateOf(true) } var cloneSubmodules by remember { mutableStateOf(true) }
val error by cloneViewModel.error.collectAsState()
val urlFocusRequester = remember { FocusRequester() } val urlFocusRequester = remember { FocusRequester() }
val directoryFocusRequester = remember { FocusRequester() } val directoryFocusRequester = remember { FocusRequester() }
val directoryButtonFocusRequester = remember { FocusRequester() } val directoryButtonFocusRequester = remember { FocusRequester() }
@ -115,10 +112,10 @@ private fun CloneInput(
previous = cancelButtonFocusRequester previous = cancelButtonFocusRequester
next = directoryFocusRequester next = directoryFocusRequester
}, },
onValueChange = { onValueChange = { repositoryUrl ->
url = repositoryUrl
cloneViewModel.onRepositoryUrlChanged(repositoryUrl)
cloneViewModel.resetStateIfError() cloneViewModel.resetStateIfError()
url = it
cloneViewModel.url = url
} }
) )
@ -138,9 +135,9 @@ private fun CloneInput(
next = directoryButtonFocusRequester next = directoryButtonFocusRequester
}, },
onValueChange = { onValueChange = {
cloneViewModel.resetStateIfError()
directory = it directory = it
cloneViewModel.directory = directory cloneViewModel.onDirectoryPathChanged(directory)
cloneViewModel.resetStateIfError()
}, },
) )
@ -149,8 +146,9 @@ private fun CloneInput(
cloneViewModel.resetStateIfError() cloneViewModel.resetStateIfError()
val newDirectory = cloneViewModel.openDirectoryPicker() val newDirectory = cloneViewModel.openDirectoryPicker()
if (newDirectory != null) { if (newDirectory != null) {
directory = newDirectory directory = TextFieldValue(newDirectory, selection = TextRange(newDirectory.count()))
cloneViewModel.directory = directory cloneViewModel.onDirectoryPathChanged(directory)
cloneViewModel.resetStateIfError()
} }
}, },
modifier = Modifier modifier = Modifier
@ -200,7 +198,7 @@ private fun CloneInput(
) )
} }
AnimatedVisibility (errorMessage != null) { AnimatedVisibility (error.isNotBlank()) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -209,7 +207,7 @@ private fun CloneInput(
.background(MaterialTheme.colors.error) .background(MaterialTheme.colors.error)
) { ) {
Text( Text(
errorMessage.orEmpty(), error,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 8.dp), .padding(vertical = 4.dp, horizontal = 8.dp),
@ -237,7 +235,7 @@ private fun CloneInput(
) )
PrimaryButton( PrimaryButton(
onClick = { onClick = {
cloneViewModel.clone(directory, url, cloneSubmodules) cloneViewModel.clone(directory.text, url.text, cloneSubmodules)
}, },
modifier = Modifier modifier = Modifier
.focusRequester(cloneButtonFocusRequester) .focusRequester(cloneButtonFocusRequester)
@ -248,6 +246,10 @@ private fun CloneInput(
text = "Clone" text = "Clone"
) )
} }
LaunchedEffect(Unit) {
urlFocusRequester.requestFocus()
}
} }
} }
@ -333,11 +335,11 @@ private fun Cancelling() {
private fun TextInput( private fun TextInput(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String, title: String,
value: String, value: TextFieldValue,
enabled: Boolean = true, enabled: Boolean = true,
focusRequester: FocusRequester, focusRequester: FocusRequester,
focusProperties: FocusProperties.() -> Unit, focusProperties: FocusProperties.() -> Unit,
onValueChange: (String) -> Unit, onValueChange: (TextFieldValue) -> Unit,
textFieldShape: Shape = RoundedCornerShape(4.dp), textFieldShape: Shape = RoundedCornerShape(4.dp),
) { ) {
Column( Column(

View File

@ -1,5 +1,6 @@
package com.jetpackduba.gitnuro.viewmodels package com.jetpackduba.gitnuro.viewmodels
import androidx.compose.ui.text.input.TextFieldValue
import com.jetpackduba.gitnuro.git.CloneState import com.jetpackduba.gitnuro.git.CloneState
import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.remote_operations.CloneRepositoryUseCase import com.jetpackduba.gitnuro.git.remote_operations.CloneRepositoryUseCase
@ -9,7 +10,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -19,25 +20,29 @@ class CloneViewModel @Inject constructor(
private val cloneRepositoryUseCase: CloneRepositoryUseCase, private val cloneRepositoryUseCase: CloneRepositoryUseCase,
private val openFilePickerUseCase: OpenFilePickerUseCase, 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>(CloneState.None) private val _cloneState = MutableStateFlow<CloneState>(CloneState.None)
val cloneState: StateFlow<CloneState> val cloneState = _cloneState.asStateFlow()
get() = _cloneState
var url: String = "" private val _error = MutableStateFlow("")
var directory: String = "" val error = _error.asStateFlow()
private var cloneJob: Job? = null private var cloneJob: Job? = null
fun clone(directoryPath: String, url: String, cloneSubmodules: Boolean) { fun clone(directoryPath: String, url: String, cloneSubmodules: Boolean) {
cloneJob = tabState.safeProcessingWithoutGit { cloneJob = tabState.safeProcessingWithoutGit {
if (directoryPath.isBlank()) { if (directoryPath.isBlank()) {
_cloneState.value = CloneState.Fail("Invalid empty directory") _error.value = "Invalid empty directory"
return@safeProcessingWithoutGit return@safeProcessingWithoutGit
} }
if (url.isBlank()) { if (url.isBlank()) {
_cloneState.value = CloneState.Fail("Invalid empty URL") _error.value = "Invalid empty URL"
return@safeProcessingWithoutGit return@safeProcessingWithoutGit
} }
@ -57,7 +62,7 @@ class CloneViewModel @Inject constructor(
} }
if (repoName.isNullOrBlank()) { if (repoName.isNullOrBlank()) {
_cloneState.value = CloneState.Fail("Check your URL and try again") _error.value = "Check your URL and try again"
return@safeProcessingWithoutGit return@safeProcessingWithoutGit
} }
@ -75,17 +80,16 @@ class CloneViewModel @Inject constructor(
cloneRepositoryUseCase(repoDir, url, cloneSubmodules) cloneRepositoryUseCase(repoDir, url, cloneSubmodules)
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.collect { newCloneStatus -> .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 { fun cancelClone() = tabState.safeProcessingWithoutGit {
_cloneState.value = CloneState.Cancelling _cloneState.value = CloneState.Cancelling
cloneJob?.cancelAndJoin() cloneJob?.cancelAndJoin()
@ -93,10 +97,18 @@ class CloneViewModel @Inject constructor(
} }
fun resetStateIfError() { fun resetStateIfError() {
_cloneState.value = CloneState.None _error.value = ""
} }
fun openDirectoryPicker(): String? { fun openDirectoryPicker(): String? {
return openFilePickerUseCase(PickerType.DIRECTORIES, null) return openFilePickerUseCase(PickerType.DIRECTORIES, null)
} }
fun onDirectoryPathChanged(directory: TextFieldValue) {
_directoryPath.value = directory
}
fun onRepositoryUrlChanged(repositoryUrl: TextFieldValue) {
_repositoryUrl.value = repositoryUrl
}
} }