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.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(
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user