Improved clone dialog features by adding more error management & directory picker
This commit is contained in:
parent
8a9c2d5fc3
commit
d662edba9d
@ -0,0 +1,3 @@
|
|||||||
|
package app.exceptions
|
||||||
|
|
||||||
|
class InvalidDirectoryException(msg: String) : GitnuroException(msg)
|
@ -4,11 +4,10 @@ import app.credentials.GSessionManager
|
|||||||
import app.credentials.HttpCredentialsProvider
|
import app.credentials.HttpCredentialsProvider
|
||||||
import app.extensions.remoteName
|
import app.extensions.remoteName
|
||||||
import app.extensions.simpleName
|
import app.extensions.simpleName
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.api.RebaseResult
|
import org.eclipse.jgit.api.RebaseResult
|
||||||
import org.eclipse.jgit.lib.ProgressMonitor
|
import org.eclipse.jgit.lib.ProgressMonitor
|
||||||
@ -16,15 +15,12 @@ import org.eclipse.jgit.lib.Ref
|
|||||||
import org.eclipse.jgit.transport.*
|
import org.eclipse.jgit.transport.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
|
||||||
class RemoteOperationsManager @Inject constructor(
|
class RemoteOperationsManager @Inject constructor(
|
||||||
private val sessionManager: GSessionManager
|
private val sessionManager: GSessionManager
|
||||||
) {
|
) {
|
||||||
private val _cloneStatus = MutableStateFlow<CloneStatus>(CloneStatus.None)
|
|
||||||
val cloneStatus: StateFlow<CloneStatus>
|
|
||||||
get() = _cloneStatus
|
|
||||||
|
|
||||||
suspend fun pull(git: Git, rebase: Boolean) = withContext(Dispatchers.IO) {
|
suspend fun pull(git: Git, rebase: Boolean) = withContext(Dispatchers.IO) {
|
||||||
val pullResult = git
|
val pullResult = git
|
||||||
.pull()
|
.pull()
|
||||||
@ -226,59 +222,69 @@ class RemoteOperationsManager @Inject constructor(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun clone(directory: File, url: String) = withContext(Dispatchers.IO) {
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun clone(directory: File, url: String): Flow<CloneStatus> = callbackFlow {
|
||||||
try {
|
try {
|
||||||
_cloneStatus.value = CloneStatus.Cloning(0)
|
ensureActive()
|
||||||
|
this.trySend(CloneStatus.Cloning(0))
|
||||||
|
|
||||||
Git.cloneRepository()
|
Git.cloneRepository()
|
||||||
.setDirectory(directory)
|
.setDirectory(directory)
|
||||||
.setURI(url)
|
.setURI(url)
|
||||||
.setProgressMonitor(object : ProgressMonitor {
|
.setProgressMonitor(
|
||||||
override fun start(totalTasks: Int) {
|
object : ProgressMonitor {
|
||||||
println("ProgressMonitor Start")
|
override fun start(totalTasks: Int) {
|
||||||
}
|
println("ProgressMonitor Start with total tasks of: $totalTasks")
|
||||||
|
}
|
||||||
|
|
||||||
override fun beginTask(title: String?, totalWork: Int) {
|
override fun beginTask(title: String?, totalWork: Int) {
|
||||||
println("ProgressMonitor Begin task")
|
println("ProgressMonitor Begin task with title: $title")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(completed: Int) {
|
override fun update(completed: Int) {
|
||||||
println("ProgressMonitor Update $completed")
|
println("ProgressMonitor Update $completed")
|
||||||
_cloneStatus.value = CloneStatus.Cloning(completed)
|
ensureActive()
|
||||||
}
|
trySend(CloneStatus.Cloning(completed))
|
||||||
|
}
|
||||||
|
|
||||||
override fun endTask() {
|
override fun endTask() {
|
||||||
println("ProgressMonitor End task")
|
println("ProgressMonitor End task")
|
||||||
_cloneStatus.value = CloneStatus.CheckingOut
|
ensureActive()
|
||||||
}
|
trySend(CloneStatus.CheckingOut)
|
||||||
|
}
|
||||||
|
|
||||||
override fun isCancelled(): Boolean {
|
override fun isCancelled(): Boolean {
|
||||||
return !isActive
|
return !isActive
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
})
|
|
||||||
.setTransportConfigCallback {
|
.setTransportConfigCallback {
|
||||||
handleTransportCredentials(it)
|
handleTransportCredentials(it)
|
||||||
}
|
}
|
||||||
.call()
|
.call()
|
||||||
|
|
||||||
_cloneStatus.value = CloneStatus.Completed
|
ensureActive()
|
||||||
|
trySend(CloneStatus.Completed(directory))
|
||||||
|
channel.close()
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
_cloneStatus.value = CloneStatus.Fail(ex.localizedMessage)
|
if(ex.cause?.cause is CancellationException) {
|
||||||
|
println("Clone cancelled")
|
||||||
|
} else {
|
||||||
|
trySend(CloneStatus.Fail(ex.localizedMessage))
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
awaitClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetCloneStatus() {
|
|
||||||
_cloneStatus.value = CloneStatus.None
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class CloneStatus {
|
sealed class CloneStatus {
|
||||||
object None : CloneStatus()
|
object None : CloneStatus()
|
||||||
data class Cloning(val progress: Int) : CloneStatus()
|
data class Cloning(val progress: Int) : CloneStatus()
|
||||||
|
object Cancelling : CloneStatus()
|
||||||
object CheckingOut : CloneStatus()
|
object CheckingOut : CloneStatus()
|
||||||
data class Fail(val reason: String) : CloneStatus()
|
data class Fail(val reason: String) : CloneStatus()
|
||||||
object Completed : CloneStatus()
|
data class Completed(val repoDir: File) : CloneStatus()
|
||||||
}
|
}
|
@ -85,14 +85,14 @@ class TabState @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun safeProcessingWihoutGit(showError: Boolean = true, callback: suspend () -> Unit) =
|
fun safeProcessingWihoutGit(showError: Boolean = true, callback: suspend CoroutineScope.() -> Unit) =
|
||||||
managerScope.launch(Dispatchers.IO) {
|
managerScope.launch(Dispatchers.IO) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
_processing.value = true
|
_processing.value = true
|
||||||
operationRunning = true
|
operationRunning = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
callback()
|
this.callback()
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
ex.printStackTrace()
|
ex.printStackTrace()
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ val secondaryTextDark = Color(0xFFCCCBCB)
|
|||||||
val borderColorLight = Color(0xFF989898)
|
val borderColorLight = Color(0xFF989898)
|
||||||
val borderColorDark = Color(0xFF989898)
|
val borderColorDark = Color(0xFF989898)
|
||||||
val errorColor = Color(0xFFc93838)
|
val errorColor = Color(0xFFc93838)
|
||||||
|
val onErrorColor = Color(0xFFFFFFFF)
|
||||||
|
|
||||||
val backgroundColorLight = Color(0xFFFFFFFF)
|
val backgroundColorLight = Color(0xFFFFFFFF)
|
||||||
val backgroundColorDark = Color(0xFF0E1621)
|
val backgroundColorDark = Color(0xFF0E1621)
|
||||||
|
@ -14,7 +14,8 @@ private val DarkColorPalette = darkColors(
|
|||||||
secondary = secondary,
|
secondary = secondary,
|
||||||
surface = surfaceColorDark,
|
surface = surfaceColorDark,
|
||||||
background = backgroundColorDark,
|
background = backgroundColorDark,
|
||||||
error = errorColor
|
error = errorColor,
|
||||||
|
onError = onErrorColor,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorPalette = lightColors(
|
private val LightColorPalette = lightColors(
|
||||||
@ -23,10 +24,8 @@ private val LightColorPalette = lightColors(
|
|||||||
secondary = secondary,
|
secondary = secondary,
|
||||||
background = backgroundColorLight,
|
background = backgroundColorLight,
|
||||||
surface = surfaceColorLight,
|
surface = surfaceColorLight,
|
||||||
error = errorColor
|
error = errorColor,
|
||||||
/* Other default colors to override
|
onError = onErrorColor,
|
||||||
|
|
||||||
*/
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -15,7 +15,6 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.DefaultAlpha
|
import androidx.compose.ui.graphics.DefaultAlpha
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -121,12 +120,12 @@ fun AppTab(
|
|||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 16.dp),
|
.padding(top = 16.dp),
|
||||||
color = Color.White,
|
color = MaterialTheme.colors.onError,
|
||||||
) // TODO Add more descriptive title
|
) // TODO Add more descriptive title
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = lastError?.message ?: "",
|
text = lastError?.message ?: "",
|
||||||
color = Color.White,
|
color = MaterialTheme.colors.onError,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 8.dp, bottom = 16.dp)
|
.padding(top = 8.dp, bottom = 16.dp)
|
||||||
.widthIn(max = 600.dp)
|
.widthIn(max = 600.dp)
|
||||||
|
@ -23,7 +23,6 @@ import app.extensions.dirPath
|
|||||||
import app.theme.primaryTextColor
|
import app.theme.primaryTextColor
|
||||||
import app.theme.secondaryTextColor
|
import app.theme.secondaryTextColor
|
||||||
import app.ui.dialogs.CloneDialog
|
import app.ui.dialogs.CloneDialog
|
||||||
import app.ui.dialogs.MaterialDialog
|
|
||||||
import app.viewmodels.TabViewModel
|
import app.viewmodels.TabViewModel
|
||||||
import openDirectoryDialog
|
import openDirectoryDialog
|
||||||
import openRepositoryDialog
|
import openRepositoryDialog
|
||||||
@ -90,7 +89,7 @@ fun WelcomePage(
|
|||||||
painter = painterResource("open.svg"),
|
painter = painterResource("open.svg"),
|
||||||
onClick = {
|
onClick = {
|
||||||
val dir = openDirectoryDialog()
|
val dir = openDirectoryDialog()
|
||||||
if(dir != null)
|
if (dir != null)
|
||||||
tabViewModel.initLocalRepository(dir)
|
tabViewModel.initLocalRepository(dir)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -163,23 +162,23 @@ fun WelcomePage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(showCloneView) {
|
||||||
|
if (showCloneView)
|
||||||
|
tabViewModel.cloneViewModel.reset() // Reset dialog before showing it
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (showCloneView)
|
if (showCloneView)
|
||||||
MaterialDialog {
|
CloneDialog(
|
||||||
CloneDialog(tabViewModel, onClose = { showCloneView = false })
|
tabViewModel.cloneViewModel,
|
||||||
}
|
onClose = {
|
||||||
// Popup(focusable = true, onDismissRequest = { showCloneView = false }, alignment = Alignment.Center) {
|
showCloneView = false
|
||||||
//
|
tabViewModel.cloneViewModel.reset()
|
||||||
// }
|
},
|
||||||
|
onOpenRepository = { dir ->
|
||||||
// PopupAlertDialogProvider.AlertDialog(onDismissRequest = {}) {
|
tabViewModel.openRepository(dir)
|
||||||
//
|
},
|
||||||
// CloneDialog(gitManager, onClose = { showCloneView = false })
|
)
|
||||||
// }
|
|
||||||
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -1,68 +1,183 @@
|
|||||||
package app.ui.dialogs
|
package app.ui.dialogs
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusOrder
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import app.git.CloneStatus
|
import app.git.CloneStatus
|
||||||
import app.theme.primaryTextColor
|
import app.theme.primaryTextColor
|
||||||
import app.viewmodels.TabViewModel
|
import app.viewmodels.CloneViewModel
|
||||||
|
import openDirectoryDialog
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CloneDialog(
|
fun CloneDialog(
|
||||||
gitManager: TabViewModel,
|
cloneViewModel: CloneViewModel,
|
||||||
onClose: () -> Unit
|
onClose: () -> Unit,
|
||||||
|
onOpenRepository: (File) -> Unit,
|
||||||
) {
|
) {
|
||||||
val cloneStatus = gitManager.cloneStatus.collectAsState()
|
val cloneStatus = cloneViewModel.cloneStatus.collectAsState()
|
||||||
val cloneStatusValue = cloneStatus.value
|
val cloneStatusValue = cloneStatus.value
|
||||||
var directory by remember { mutableStateOf("") }
|
|
||||||
var url by remember { mutableStateOf("") }
|
|
||||||
Column {
|
|
||||||
if (cloneStatusValue is CloneStatus.Cloning || cloneStatusValue == CloneStatus.CheckingOut)
|
|
||||||
LinearProgressIndicator(modifier = Modifier.width(500.dp))
|
|
||||||
else if (cloneStatusValue == CloneStatus.Completed) {
|
|
||||||
gitManager.openRepository(directory)
|
|
||||||
onClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
MaterialDialog {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(400.dp)
|
||||||
|
.animateContentSize()
|
||||||
|
) {
|
||||||
|
when (cloneStatusValue) {
|
||||||
|
CloneStatus.CheckingOut -> {
|
||||||
|
Cloning(cloneViewModel)
|
||||||
|
}
|
||||||
|
is CloneStatus.Cloning -> {
|
||||||
|
Cloning(cloneViewModel)
|
||||||
|
}
|
||||||
|
is CloneStatus.Cancelling -> {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
is CloneStatus.Completed -> {
|
||||||
|
onOpenRepository(cloneStatusValue.repoDir)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
is CloneStatus.Fail -> CloneInput(
|
||||||
|
cloneViewModel = cloneViewModel,
|
||||||
|
onClose = onClose,
|
||||||
|
errorMessage = cloneStatusValue.reason
|
||||||
|
)
|
||||||
|
CloneStatus.None -> CloneInput(
|
||||||
|
cloneViewModel = cloneViewModel,
|
||||||
|
onClose = onClose,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CloneInput(
|
||||||
|
cloneViewModel: CloneViewModel,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
errorMessage: String? = null,
|
||||||
|
) {
|
||||||
|
var url by remember { mutableStateOf(cloneViewModel.url) }
|
||||||
|
var directory by remember { mutableStateOf(cloneViewModel.directory) }
|
||||||
|
var errorHasBeenNoticed by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val urlFocusRequester = remember { FocusRequester() }
|
||||||
|
val directoryFocusRequester = remember { FocusRequester() }
|
||||||
|
val directoryButtonFocusRequester = remember { FocusRequester() }
|
||||||
|
val cloneButtonFocusRequester = remember { FocusRequester() }
|
||||||
|
val cancelButtonFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
urlFocusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Clone a repository",
|
"Clone a new repository",
|
||||||
color = MaterialTheme.colors.primaryTextColor,
|
color = MaterialTheme.colors.primaryTextColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp, horizontal = 8.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(400.dp)
|
.fillMaxWidth()
|
||||||
.padding(vertical = 4.dp, horizontal = 8.dp),
|
.padding(vertical = 4.dp, horizontal = 8.dp)
|
||||||
|
.focusOrder(urlFocusRequester) {
|
||||||
|
previous = cancelButtonFocusRequester
|
||||||
|
next = directoryFocusRequester
|
||||||
|
},
|
||||||
label = { Text("URL") },
|
label = { Text("URL") },
|
||||||
textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
|
textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
value = url,
|
value = url,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
|
errorHasBeenNoticed = true
|
||||||
url = it
|
url = it
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
OutlinedTextField(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(400.dp)
|
.fillMaxWidth()
|
||||||
.padding(vertical = 4.dp, horizontal = 8.dp),
|
.padding(vertical = 4.dp, horizontal = 8.dp),
|
||||||
label = { Text("Directory") },
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
|
) {
|
||||||
maxLines = 1,
|
OutlinedTextField(
|
||||||
value = directory,
|
modifier = Modifier
|
||||||
onValueChange = {
|
.weight(1f)
|
||||||
directory = it
|
.padding(end = 4.dp)
|
||||||
|
.focusOrder(directoryFocusRequester) {
|
||||||
|
previous = urlFocusRequester
|
||||||
|
next = directoryButtonFocusRequester
|
||||||
|
},
|
||||||
|
textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
|
||||||
|
maxLines = 1,
|
||||||
|
label = { Text("Directory") },
|
||||||
|
value = directory,
|
||||||
|
onValueChange = {
|
||||||
|
errorHasBeenNoticed = true
|
||||||
|
directory = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
errorHasBeenNoticed = true
|
||||||
|
val newDirectory = openDirectoryDialog()
|
||||||
|
if (newDirectory != null)
|
||||||
|
directory = newDirectory
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.focusOrder(directoryButtonFocusRequester) {
|
||||||
|
previous = directoryFocusRequester
|
||||||
|
next = cloneButtonFocusRequester
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.primaryTextColor,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(errorMessage != null && !errorHasBeenNoticed) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp, horizontal = 8.dp)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(MaterialTheme.colors.error)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
errorMessage.orEmpty(),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp, horizontal = 8.dp),
|
||||||
|
color = MaterialTheme.colors.onError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -70,7 +185,12 @@ fun CloneDialog(
|
|||||||
.align(Alignment.End)
|
.align(Alignment.End)
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
modifier = Modifier
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
.focusOrder(cancelButtonFocusRequester) {
|
||||||
|
previous = cloneButtonFocusRequester
|
||||||
|
next = urlFocusRequester
|
||||||
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
@ -79,11 +199,61 @@ fun CloneDialog(
|
|||||||
}
|
}
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
gitManager.clone(File(directory), url)
|
cloneViewModel.clone(directory, url)
|
||||||
}
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.focusOrder(cloneButtonFocusRequester) {
|
||||||
|
previous = directoryButtonFocusRequester
|
||||||
|
next = cancelButtonFocusRequester
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Text("Clone")
|
Text("Clone")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Cloning(cloneViewModel: CloneViewModel) {
|
||||||
|
Column (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
|
||||||
|
CircularProgressIndicator(modifier = Modifier.padding(horizontal = 16.dp))
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
top = 36.dp,
|
||||||
|
end = 8.dp
|
||||||
|
)
|
||||||
|
.align(Alignment.End),
|
||||||
|
onClick = {
|
||||||
|
cloneViewModel.cancelClone()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Cancelling() {
|
||||||
|
Column (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Cancelling clone operation...",
|
||||||
|
color = MaterialTheme.colors.primaryTextColor,
|
||||||
|
modifier = Modifier.padding(vertical = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
86
src/main/kotlin/app/viewmodels/CloneViewModel.kt
Normal file
86
src/main/kotlin/app/viewmodels/CloneViewModel.kt
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package app.viewmodels
|
||||||
|
|
||||||
|
import app.git.CloneStatus
|
||||||
|
import app.git.RemoteOperationsManager
|
||||||
|
import app.git.TabState
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class CloneViewModel @Inject constructor(
|
||||||
|
private val tabState: TabState,
|
||||||
|
private val remoteOperationsManager: RemoteOperationsManager,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val _cloneStatus = MutableStateFlow<CloneStatus>(CloneStatus.None)
|
||||||
|
val cloneStatus: StateFlow<CloneStatus>
|
||||||
|
get() = _cloneStatus
|
||||||
|
|
||||||
|
var url: String = ""
|
||||||
|
var directory: String = ""
|
||||||
|
|
||||||
|
private var cloneJob: Job? = null
|
||||||
|
|
||||||
|
fun clone(directoryPath: String, url: String) {
|
||||||
|
cloneJob = tabState.safeProcessingWihoutGit {
|
||||||
|
if (directoryPath.isBlank()) {
|
||||||
|
_cloneStatus.value = CloneStatus.Fail("Check your URL and try again")
|
||||||
|
return@safeProcessingWihoutGit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.isBlank()) {
|
||||||
|
_cloneStatus.value = CloneStatus.Fail("Check your URL and try again")
|
||||||
|
return@safeProcessingWihoutGit
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val urlSplit = url.split("/", "\\").toMutableList()
|
||||||
|
|
||||||
|
// Removes the last element for URLs that end with "/" or "\" instead of the repo name like https://github.com/JetpackDuba/Gitnuro/
|
||||||
|
if(urlSplit.isNotEmpty() && urlSplit.last().isBlank()) {
|
||||||
|
urlSplit.removeLast()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the last element of the path/URL to generate obtain the repo name
|
||||||
|
val repoName = urlSplit.lastOrNull()?.replace(".git", "")
|
||||||
|
|
||||||
|
if (repoName.isNullOrBlank()) {
|
||||||
|
_cloneStatus.value = CloneStatus.Fail("Check your URL and try again")
|
||||||
|
return@safeProcessingWihoutGit
|
||||||
|
}
|
||||||
|
|
||||||
|
val directory = File(directoryPath)
|
||||||
|
|
||||||
|
if (!directory.exists()) {
|
||||||
|
directory.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
val repoDir = File(directory, repoName)
|
||||||
|
if (!repoDir.exists()) {
|
||||||
|
repoDir.mkdir()
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteOperationsManager.clone(repoDir, url)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { newCloneStatus ->
|
||||||
|
_cloneStatus.value = newCloneStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
_cloneStatus.value = CloneStatus.None
|
||||||
|
url = ""
|
||||||
|
directory = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelClone() {
|
||||||
|
cloneJob?.cancel()
|
||||||
|
_cloneStatus.value = CloneStatus.Cancelling
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,11 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
private const val MIN_TIME_IN_MS_BETWEEN_REFRESHES = 500L
|
private const val MIN_TIME_IN_MS_BETWEEN_REFRESHES = 500L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains all the information related to a tab and its subcomponents (smaller composables like the log, branches,
|
||||||
|
* commit changes, etc.). It holds a reference to every view model because this class lives as long as the tab is open (survives
|
||||||
|
* across full app recompositions), therefore, tab's content can be recreated with these view models.
|
||||||
|
*/
|
||||||
class TabViewModel @Inject constructor(
|
class TabViewModel @Inject constructor(
|
||||||
val logViewModel: LogViewModel,
|
val logViewModel: LogViewModel,
|
||||||
val branchesViewModel: BranchesViewModel,
|
val branchesViewModel: BranchesViewModel,
|
||||||
@ -29,8 +34,8 @@ class TabViewModel @Inject constructor(
|
|||||||
val menuViewModel: MenuViewModel,
|
val menuViewModel: MenuViewModel,
|
||||||
val stashesViewModel: StashesViewModel,
|
val stashesViewModel: StashesViewModel,
|
||||||
val commitChangesViewModel: CommitChangesViewModel,
|
val commitChangesViewModel: CommitChangesViewModel,
|
||||||
|
val cloneViewModel: CloneViewModel,
|
||||||
private val repositoryManager: RepositoryManager,
|
private val repositoryManager: RepositoryManager,
|
||||||
private val remoteOperationsManager: RemoteOperationsManager,
|
|
||||||
private val tabState: TabState,
|
private val tabState: TabState,
|
||||||
val appStateManager: AppStateManager,
|
val appStateManager: AppStateManager,
|
||||||
private val fileChangesWatcher: FileChangesWatcher,
|
private val fileChangesWatcher: FileChangesWatcher,
|
||||||
@ -47,7 +52,6 @@ class TabViewModel @Inject constructor(
|
|||||||
val processing: StateFlow<Boolean> = tabState.processing
|
val processing: StateFlow<Boolean> = tabState.processing
|
||||||
|
|
||||||
val credentialsState: StateFlow<CredentialsState> = credentialsStateManager.credentialsState
|
val credentialsState: StateFlow<CredentialsState> = credentialsStateManager.credentialsState
|
||||||
val cloneStatus: StateFlow<CloneStatus> = remoteOperationsManager.cloneStatus
|
|
||||||
|
|
||||||
private val _diffSelected = MutableStateFlow<DiffEntryType?>(null)
|
private val _diffSelected = MutableStateFlow<DiffEntryType?>(null)
|
||||||
val diffSelected: StateFlow<DiffEntryType?> = _diffSelected
|
val diffSelected: StateFlow<DiffEntryType?> = _diffSelected
|
||||||
@ -243,10 +247,6 @@ class TabViewModel @Inject constructor(
|
|||||||
tabState.managerScope.cancel()
|
tabState.managerScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clone(directory: File, url: String) = tabState.safeProcessingWihoutGit {
|
|
||||||
remoteOperationsManager.clone(directory, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateDiffEntry() {
|
private fun updateDiffEntry() {
|
||||||
val diffSelected = diffSelected.value
|
val diffSelected = diffSelected.value
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user