Fixed UX issues with clone dialog
This commit is contained in:
parent
73816089a6
commit
2e825be44b
@ -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(
|
||||
|
@ -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>(CloneState.None)
|
||||
val cloneState: StateFlow<CloneState>
|
||||
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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user