Code refactoring
This commit is contained in:
parent
a9a35b304a
commit
355cbc3f79
@ -1,9 +0,0 @@
|
|||||||
package com.jetpackduba.gitnuro.extensions
|
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
|
|
||||||
fun LazyListState.observeScrollChanges() {
|
|
||||||
// When accessing this property, this parent composable is recomposed when the scroll changes
|
|
||||||
// because LazyListState is marked with @Stable
|
|
||||||
this.firstVisibleItemScrollOffset
|
|
||||||
}
|
|
@ -39,6 +39,7 @@ class TabState @Inject constructor(
|
|||||||
val selectedItem: StateFlow<SelectedItem> = _selectedItem
|
val selectedItem: StateFlow<SelectedItem> = _selectedItem
|
||||||
private val _taskEvent = MutableSharedFlow<TaskEvent>()
|
private val _taskEvent = MutableSharedFlow<TaskEvent>()
|
||||||
val taskEvent: SharedFlow<TaskEvent> = _taskEvent
|
val taskEvent: SharedFlow<TaskEvent> = _taskEvent
|
||||||
|
|
||||||
var lastOperation: Long = 0
|
var lastOperation: Long = 0
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@ -52,8 +53,11 @@ class TabState @Inject constructor(
|
|||||||
return unsafeGit
|
return unsafeGit
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _refreshData = MutableSharedFlow<RefreshType>()
|
private val refreshData = MutableSharedFlow<RefreshType>()
|
||||||
val refreshData: SharedFlow<RefreshType> = _refreshData
|
private val closeableViews = ArrayDeque<Int>()
|
||||||
|
|
||||||
|
private val _closeView = MutableSharedFlow<Int>()
|
||||||
|
val closeViewFlow = _closeView.asSharedFlow()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Property that indicates if a git operation is running
|
* Property that indicates if a git operation is running
|
||||||
@ -130,7 +134,7 @@ class TabState @Inject constructor(
|
|||||||
lastOperation = System.currentTimeMillis()
|
lastOperation = System.currentTimeMillis()
|
||||||
|
|
||||||
if (refreshType != RefreshType.NONE && (!hasProcessFailed || refreshEvenIfCrashes || refreshEvenIfCrashesInteractiveResult)) {
|
if (refreshType != RefreshType.NONE && (!hasProcessFailed || refreshEvenIfCrashes || refreshEvenIfCrashesInteractiveResult)) {
|
||||||
_refreshData.emit(refreshType)
|
refreshData.emit(refreshType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,7 +232,7 @@ class TabState @Inject constructor(
|
|||||||
printError(TAG, ex.message.orEmpty(), ex)
|
printError(TAG, ex.message.orEmpty(), ex)
|
||||||
} finally {
|
} finally {
|
||||||
if (refreshType != RefreshType.NONE && (!hasProcessFailed || refreshEvenIfCrashes))
|
if (refreshType != RefreshType.NONE && (!hasProcessFailed || refreshEvenIfCrashes))
|
||||||
_refreshData.emit(refreshType)
|
refreshData.emit(refreshType)
|
||||||
|
|
||||||
operationRunning = false
|
operationRunning = false
|
||||||
lastOperation = System.currentTimeMillis()
|
lastOperation = System.currentTimeMillis()
|
||||||
@ -236,7 +240,7 @@ class TabState @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun refreshData(refreshType: RefreshType) {
|
suspend fun refreshData(refreshType: RefreshType) {
|
||||||
_refreshData.emit(refreshType)
|
refreshData.emit(refreshType)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun newSelectedStash(stash: RevCommit) {
|
suspend fun newSelectedStash(stash: RevCommit) {
|
||||||
@ -307,6 +311,22 @@ class TabState @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addCloseableView(id: Int) {
|
||||||
|
closeableViews.add(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCloseableView(id: Int) {
|
||||||
|
closeableViews.remove(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun closeLastView() {
|
||||||
|
val last = closeableViews.removeLastOrNull()
|
||||||
|
|
||||||
|
if (last != null) {
|
||||||
|
_closeView.emit(last)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun cancelCurrentTask() {
|
fun cancelCurrentTask() {
|
||||||
currentJob?.cancel()
|
currentJob?.cancel()
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,19 @@
|
|||||||
package com.jetpackduba.gitnuro.ui
|
package com.jetpackduba.gitnuro.ui
|
||||||
|
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
|
||||||
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.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.jetpackduba.gitnuro.AppIcons
|
|
||||||
import com.jetpackduba.gitnuro.LoadingRepository
|
import com.jetpackduba.gitnuro.LoadingRepository
|
||||||
import com.jetpackduba.gitnuro.ProcessingScreen
|
import com.jetpackduba.gitnuro.ProcessingScreen
|
||||||
import com.jetpackduba.gitnuro.credentials.CredentialsAccepted
|
|
||||||
import com.jetpackduba.gitnuro.credentials.CredentialsRequested
|
|
||||||
import com.jetpackduba.gitnuro.credentials.CredentialsState
|
|
||||||
import com.jetpackduba.gitnuro.git.ProcessingState
|
import com.jetpackduba.gitnuro.git.ProcessingState
|
||||||
import com.jetpackduba.gitnuro.models.Notification
|
import com.jetpackduba.gitnuro.ui.components.Notification
|
||||||
import com.jetpackduba.gitnuro.models.NotificationType
|
|
||||||
import com.jetpackduba.gitnuro.theme.AppTheme
|
|
||||||
import com.jetpackduba.gitnuro.ui.dialogs.CloneDialog
|
import com.jetpackduba.gitnuro.ui.dialogs.CloneDialog
|
||||||
import com.jetpackduba.gitnuro.ui.dialogs.GpgPasswordDialog
|
import com.jetpackduba.gitnuro.ui.dialogs.CredentialsDialog
|
||||||
import com.jetpackduba.gitnuro.ui.dialogs.SshPasswordDialog
|
|
||||||
import com.jetpackduba.gitnuro.ui.dialogs.UserPasswordDialog
|
|
||||||
import com.jetpackduba.gitnuro.ui.dialogs.errors.ErrorDialog
|
import com.jetpackduba.gitnuro.ui.dialogs.errors.ErrorDialog
|
||||||
import com.jetpackduba.gitnuro.ui.dialogs.settings.SettingsDialog
|
import com.jetpackduba.gitnuro.ui.dialogs.settings.SettingsDialog
|
||||||
import com.jetpackduba.gitnuro.viewmodels.RepositorySelectionStatus
|
import com.jetpackduba.gitnuro.viewmodels.RepositorySelectionStatus
|
||||||
@ -109,7 +94,7 @@ fun AppTab(
|
|||||||
|
|
||||||
is RepositorySelectionStatus.Open -> {
|
is RepositorySelectionStatus.Open -> {
|
||||||
RepositoryOpenPage(
|
RepositoryOpenPage(
|
||||||
tabViewModel = tabViewModel,
|
repositoryOpenViewModel = tabViewModel.repositoryOpenViewModel,
|
||||||
onShowSettingsDialog = { showSettingsDialog = true },
|
onShowSettingsDialog = { showSettingsDialog = true },
|
||||||
onShowCloneDialog = { showCloneDialog = true },
|
onShowCloneDialog = { showCloneDialog = true },
|
||||||
)
|
)
|
||||||
@ -148,119 +133,3 @@ fun AppTab(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun NotificationSuccessPreview() {
|
|
||||||
AppTheme(customTheme = null) {
|
|
||||||
Notification(NotificationType.Positive, "Hello world!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Notification(notification: Notification) {
|
|
||||||
val notificationShape = RoundedCornerShape(8.dp)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.border(2.dp, MaterialTheme.colors.onBackground.copy(0.2f), notificationShape)
|
|
||||||
.clip(notificationShape)
|
|
||||||
.background(MaterialTheme.colors.background)
|
|
||||||
.height(IntrinsicSize.Max)
|
|
||||||
.padding(2.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
val backgroundColor = when (notification.type) {
|
|
||||||
NotificationType.Positive -> MaterialTheme.colors.primary
|
|
||||||
NotificationType.Warning -> MaterialTheme.colors.secondary
|
|
||||||
NotificationType.Error -> MaterialTheme.colors.error
|
|
||||||
}
|
|
||||||
|
|
||||||
val contentColor = when (notification.type) {
|
|
||||||
NotificationType.Positive -> MaterialTheme.colors.onPrimary
|
|
||||||
NotificationType.Warning -> MaterialTheme.colors.onSecondary
|
|
||||||
NotificationType.Error -> MaterialTheme.colors.onError
|
|
||||||
}
|
|
||||||
|
|
||||||
val icon = when (notification.type) {
|
|
||||||
NotificationType.Positive -> AppIcons.INFO
|
|
||||||
NotificationType.Warning -> AppIcons.WARNING
|
|
||||||
NotificationType.Error -> AppIcons.ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(topStart = 6.dp, bottomStart = 6.dp))
|
|
||||||
.background(backgroundColor)
|
|
||||||
.fillMaxHeight()
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painterResource(icon),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = contentColor,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(4.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 8.dp)
|
|
||||||
.fillMaxHeight(),
|
|
||||||
contentAlignment = Alignment.CenterStart,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = notification.text,
|
|
||||||
modifier = Modifier,
|
|
||||||
color = MaterialTheme.colors.onBackground,
|
|
||||||
style = MaterialTheme.typography.body1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CredentialsDialog(gitManager: TabViewModel) {
|
|
||||||
val credentialsState = gitManager.credentialsState.collectAsState()
|
|
||||||
|
|
||||||
when (val credentialsStateValue = credentialsState.value) {
|
|
||||||
CredentialsRequested.HttpCredentialsRequested -> {
|
|
||||||
UserPasswordDialog(
|
|
||||||
onReject = {
|
|
||||||
gitManager.credentialsDenied()
|
|
||||||
},
|
|
||||||
onAccept = { user, password ->
|
|
||||||
gitManager.httpCredentialsAccepted(user, password)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
CredentialsRequested.SshCredentialsRequested -> {
|
|
||||||
SshPasswordDialog(
|
|
||||||
onReject = {
|
|
||||||
gitManager.credentialsDenied()
|
|
||||||
},
|
|
||||||
onAccept = { password ->
|
|
||||||
gitManager.sshCredentialsAccepted(password)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is CredentialsRequested.GpgCredentialsRequested -> {
|
|
||||||
GpgPasswordDialog(
|
|
||||||
gpgCredentialsRequested = credentialsStateValue,
|
|
||||||
onReject = {
|
|
||||||
gitManager.credentialsDenied()
|
|
||||||
},
|
|
||||||
onAccept = { password ->
|
|
||||||
gitManager.gpgCredentialsAccepted(password)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is CredentialsAccepted, CredentialsState.None, CredentialsState.CredentialsDenied -> { /* Nothing to do */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -29,7 +29,7 @@ import com.jetpackduba.gitnuro.ui.diff.Diff
|
|||||||
import com.jetpackduba.gitnuro.ui.log.Log
|
import com.jetpackduba.gitnuro.ui.log.Log
|
||||||
import com.jetpackduba.gitnuro.updates.Update
|
import com.jetpackduba.gitnuro.updates.Update
|
||||||
import com.jetpackduba.gitnuro.viewmodels.BlameState
|
import com.jetpackduba.gitnuro.viewmodels.BlameState
|
||||||
import com.jetpackduba.gitnuro.viewmodels.TabViewModel
|
import com.jetpackduba.gitnuro.viewmodels.RepositoryOpenViewModel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.eclipse.jgit.lib.RepositoryState
|
import org.eclipse.jgit.lib.RepositoryState
|
||||||
@ -37,16 +37,16 @@ import org.eclipse.jgit.revwalk.RevCommit
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RepositoryOpenPage(
|
fun RepositoryOpenPage(
|
||||||
tabViewModel: TabViewModel,
|
repositoryOpenViewModel: RepositoryOpenViewModel,
|
||||||
onShowSettingsDialog: () -> Unit,
|
onShowSettingsDialog: () -> Unit,
|
||||||
onShowCloneDialog: () -> Unit,
|
onShowCloneDialog: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val repositoryState by tabViewModel.repositoryState.collectAsState()
|
val repositoryState by repositoryOpenViewModel.repositoryState.collectAsState()
|
||||||
val diffSelected by tabViewModel.diffSelected.collectAsState()
|
val diffSelected by repositoryOpenViewModel.diffSelected.collectAsState()
|
||||||
val selectedItem by tabViewModel.selectedItem.collectAsState()
|
val selectedItem by repositoryOpenViewModel.selectedItem.collectAsState()
|
||||||
val blameState by tabViewModel.blameState.collectAsState()
|
val blameState by repositoryOpenViewModel.blameState.collectAsState()
|
||||||
val showHistory by tabViewModel.showHistory.collectAsState()
|
val showHistory by repositoryOpenViewModel.showHistory.collectAsState()
|
||||||
val showAuthorInfo by tabViewModel.showAuthorInfo.collectAsState()
|
val showAuthorInfo by repositoryOpenViewModel.showAuthorInfo.collectAsState()
|
||||||
|
|
||||||
var showNewBranchDialog by remember { mutableStateOf(false) }
|
var showNewBranchDialog by remember { mutableStateOf(false) }
|
||||||
var showStashWithMessageDialog by remember { mutableStateOf(false) }
|
var showStashWithMessageDialog by remember { mutableStateOf(false) }
|
||||||
@ -59,7 +59,7 @@ fun RepositoryOpenPage(
|
|||||||
showNewBranchDialog = false
|
showNewBranchDialog = false
|
||||||
},
|
},
|
||||||
onAccept = { branchName ->
|
onAccept = { branchName ->
|
||||||
tabViewModel.createBranch(branchName)
|
repositoryOpenViewModel.createBranch(branchName)
|
||||||
showNewBranchDialog = false
|
showNewBranchDialog = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -69,17 +69,17 @@ fun RepositoryOpenPage(
|
|||||||
showStashWithMessageDialog = false
|
showStashWithMessageDialog = false
|
||||||
},
|
},
|
||||||
onAccept = { stashMessage ->
|
onAccept = { stashMessage ->
|
||||||
tabViewModel.stashWithMessage(stashMessage)
|
repositoryOpenViewModel.stashWithMessage(stashMessage)
|
||||||
showStashWithMessageDialog = false
|
showStashWithMessageDialog = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else if (showAuthorInfo) {
|
} else if (showAuthorInfo) {
|
||||||
val authorViewModel = tabViewModel.authorViewModel
|
val authorViewModel = repositoryOpenViewModel.authorViewModel
|
||||||
if (authorViewModel != null) {
|
if (authorViewModel != null) {
|
||||||
AuthorDialog(
|
AuthorDialog(
|
||||||
authorViewModel = authorViewModel,
|
authorViewModel = authorViewModel,
|
||||||
onClose = {
|
onClose = {
|
||||||
tabViewModel.closeAuthorInfoDialog()
|
repositoryOpenViewModel.closeAuthorInfoDialog()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -89,16 +89,16 @@ fun RepositoryOpenPage(
|
|||||||
onAction = {
|
onAction = {
|
||||||
showQuickActionsDialog = false
|
showQuickActionsDialog = false
|
||||||
when (it) {
|
when (it) {
|
||||||
QuickActionType.OPEN_DIR_IN_FILE_MANAGER -> tabViewModel.openFolderInFileExplorer()
|
QuickActionType.OPEN_DIR_IN_FILE_MANAGER -> repositoryOpenViewModel.openFolderInFileExplorer()
|
||||||
QuickActionType.CLONE -> onShowCloneDialog()
|
QuickActionType.CLONE -> onShowCloneDialog()
|
||||||
QuickActionType.REFRESH -> tabViewModel.refreshAll()
|
QuickActionType.REFRESH -> repositoryOpenViewModel.refreshAll()
|
||||||
QuickActionType.SIGN_OFF -> showSignOffDialog = true
|
QuickActionType.SIGN_OFF -> showSignOffDialog = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else if (showSignOffDialog) {
|
} else if (showSignOffDialog) {
|
||||||
SignOffDialog(
|
SignOffDialog(
|
||||||
viewModel = tabViewModel.tabViewModelsProvider.signOffDialogViewModel,
|
viewModel = repositoryOpenViewModel.tabViewModelsProvider.signOffDialogViewModel,
|
||||||
onClose = { showSignOffDialog = false },
|
onClose = { showSignOffDialog = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -113,11 +113,11 @@ fun RepositoryOpenPage(
|
|||||||
println("Key event $it")
|
println("Key event $it")
|
||||||
when {
|
when {
|
||||||
it.matchesBinding(KeybindingOption.PULL) -> {
|
it.matchesBinding(KeybindingOption.PULL) -> {
|
||||||
tabViewModel.pull(PullType.DEFAULT)
|
repositoryOpenViewModel.pull(PullType.DEFAULT)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
it.matchesBinding(KeybindingOption.PUSH) -> {
|
it.matchesBinding(KeybindingOption.PUSH) -> {
|
||||||
tabViewModel.push()
|
repositoryOpenViewModel.push()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
it.matchesBinding(KeybindingOption.BRANCH_CREATE) -> {
|
it.matchesBinding(KeybindingOption.BRANCH_CREATE) -> {
|
||||||
@ -129,11 +129,15 @@ fun RepositoryOpenPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
it.matchesBinding(KeybindingOption.STASH) -> {
|
it.matchesBinding(KeybindingOption.STASH) -> {
|
||||||
tabViewModel.stash()
|
repositoryOpenViewModel.stash()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
it.matchesBinding(KeybindingOption.STASH_POP) -> {
|
it.matchesBinding(KeybindingOption.STASH_POP) -> {
|
||||||
tabViewModel.popStash()
|
repositoryOpenViewModel.popStash()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
it.matchesBinding(KeybindingOption.EXIT) -> {
|
||||||
|
repositoryOpenViewModel.closeLastView()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
@ -148,7 +152,7 @@ fun RepositoryOpenPage(
|
|||||||
.focusable()
|
.focusable()
|
||||||
.onKeyEvent { keyEvent ->
|
.onKeyEvent { keyEvent ->
|
||||||
if (keyEvent.matchesBinding(KeybindingOption.REFRESH)) {
|
if (keyEvent.matchesBinding(KeybindingOption.REFRESH)) {
|
||||||
tabViewModel.refreshAll()
|
repositoryOpenViewModel.refreshAll()
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@ -157,7 +161,7 @@ fun RepositoryOpenPage(
|
|||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Menu(
|
Menu(
|
||||||
menuViewModel = tabViewModel.tabViewModelsProvider.menuViewModel,
|
menuViewModel = repositoryOpenViewModel.tabViewModelsProvider.menuViewModel,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(
|
.padding(
|
||||||
vertical = 4.dp
|
vertical = 4.dp
|
||||||
@ -165,12 +169,12 @@ fun RepositoryOpenPage(
|
|||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
onCreateBranch = { showNewBranchDialog = true },
|
onCreateBranch = { showNewBranchDialog = true },
|
||||||
onStashWithMessage = { showStashWithMessageDialog = true },
|
onStashWithMessage = { showStashWithMessageDialog = true },
|
||||||
onOpenAnotherRepository = { tabViewModel.openAnotherRepository(it) },
|
onOpenAnotherRepository = { repositoryOpenViewModel.openAnotherRepository(it) },
|
||||||
onOpenAnotherRepositoryFromPicker = {
|
onOpenAnotherRepositoryFromPicker = {
|
||||||
val repoToOpen = tabViewModel.openDirectoryPicker()
|
val repoToOpen = repositoryOpenViewModel.openDirectoryPicker()
|
||||||
|
|
||||||
if (repoToOpen != null) {
|
if (repoToOpen != null) {
|
||||||
tabViewModel.openAnotherRepository(repoToOpen)
|
repositoryOpenViewModel.openAnotherRepository(repoToOpen)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onQuickActions = { showQuickActionsDialog = true },
|
onQuickActions = { showQuickActionsDialog = true },
|
||||||
@ -178,7 +182,7 @@ fun RepositoryOpenPage(
|
|||||||
)
|
)
|
||||||
|
|
||||||
RepoContent(
|
RepoContent(
|
||||||
tabViewModel = tabViewModel,
|
repositoryOpenViewModel = repositoryOpenViewModel,
|
||||||
diffSelected = diffSelected,
|
diffSelected = diffSelected,
|
||||||
selectedItem = selectedItem,
|
selectedItem = selectedItem,
|
||||||
repositoryState = repositoryState,
|
repositoryState = repositoryState,
|
||||||
@ -197,14 +201,14 @@ fun RepositoryOpenPage(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
val userInfo by tabViewModel.authorInfoSimple.collectAsState()
|
val userInfo by repositoryOpenViewModel.authorInfoSimple.collectAsState()
|
||||||
val newUpdate = tabViewModel.update.collectAsState().value
|
val newUpdate = repositoryOpenViewModel.update.collectAsState().value
|
||||||
|
|
||||||
BottomInfoBar(
|
BottomInfoBar(
|
||||||
userInfo,
|
userInfo,
|
||||||
newUpdate,
|
newUpdate,
|
||||||
onOpenUrlInBrowser = { tabViewModel.openUrlInBrowser(it) },
|
onOpenUrlInBrowser = { repositoryOpenViewModel.openUrlInBrowser(it) },
|
||||||
onShowAuthorInfoDialog = { tabViewModel.showAuthorInfoDialog() },
|
onShowAuthorInfoDialog = { repositoryOpenViewModel.showAuthorInfoDialog() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -258,7 +262,7 @@ private fun BottomInfoBar(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RepoContent(
|
fun RepoContent(
|
||||||
tabViewModel: TabViewModel,
|
repositoryOpenViewModel: RepositoryOpenViewModel,
|
||||||
diffSelected: DiffType?,
|
diffSelected: DiffType?,
|
||||||
selectedItem: SelectedItem,
|
selectedItem: SelectedItem,
|
||||||
repositoryState: RepositoryState,
|
repositoryState: RepositoryState,
|
||||||
@ -266,19 +270,19 @@ fun RepoContent(
|
|||||||
showHistory: Boolean,
|
showHistory: Boolean,
|
||||||
) {
|
) {
|
||||||
if (showHistory) {
|
if (showHistory) {
|
||||||
val historyViewModel = tabViewModel.historyViewModel
|
val historyViewModel = repositoryOpenViewModel.historyViewModel
|
||||||
|
|
||||||
if (historyViewModel != null) {
|
if (historyViewModel != null) {
|
||||||
FileHistory(
|
FileHistory(
|
||||||
historyViewModel = historyViewModel,
|
historyViewModel = historyViewModel,
|
||||||
onClose = {
|
onClose = {
|
||||||
tabViewModel.closeHistory()
|
repositoryOpenViewModel.closeHistory()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
MainContentView(
|
MainContentView(
|
||||||
tabViewModel,
|
repositoryOpenViewModel,
|
||||||
diffSelected,
|
diffSelected,
|
||||||
selectedItem,
|
selectedItem,
|
||||||
repositoryState,
|
repositoryState,
|
||||||
@ -289,25 +293,25 @@ fun RepoContent(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainContentView(
|
fun MainContentView(
|
||||||
tabViewModel: TabViewModel,
|
repositoryOpenViewModel: RepositoryOpenViewModel,
|
||||||
diffSelected: DiffType?,
|
diffSelected: DiffType?,
|
||||||
selectedItem: SelectedItem,
|
selectedItem: SelectedItem,
|
||||||
repositoryState: RepositoryState,
|
repositoryState: RepositoryState,
|
||||||
blameState: BlameState,
|
blameState: BlameState,
|
||||||
) {
|
) {
|
||||||
val rebaseInteractiveState by tabViewModel.rebaseInteractiveState.collectAsState()
|
val rebaseInteractiveState by repositoryOpenViewModel.rebaseInteractiveState.collectAsState()
|
||||||
val density = LocalDensity.current.density
|
val density = LocalDensity.current.density
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
// We create 2 mutableStates here because using directly the flow makes compose lose some drag events for some reason
|
// We create 2 mutableStates here because using directly the flow makes compose lose some drag events for some reason
|
||||||
var firstWidth by remember(tabViewModel) { mutableStateOf(tabViewModel.firstPaneWidth.value) }
|
var firstWidth by remember(repositoryOpenViewModel) { mutableStateOf(repositoryOpenViewModel.firstPaneWidth.value) }
|
||||||
var thirdWidth by remember(tabViewModel) { mutableStateOf(tabViewModel.thirdPaneWidth.value) }
|
var thirdWidth by remember(repositoryOpenViewModel) { mutableStateOf(repositoryOpenViewModel.thirdPaneWidth.value) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Update the pane widths if they have been changed in a different tab
|
// Update the pane widths if they have been changed in a different tab
|
||||||
tabViewModel.onPanelsWidthPersisted.collectLatest {
|
repositoryOpenViewModel.onPanelsWidthPersisted.collectLatest {
|
||||||
firstWidth = tabViewModel.firstPaneWidth.value
|
firstWidth = repositoryOpenViewModel.firstPaneWidth.value
|
||||||
thirdWidth = tabViewModel.thirdPaneWidth.value
|
thirdWidth = repositoryOpenViewModel.thirdPaneWidth.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,9 +321,9 @@ fun MainContentView(
|
|||||||
thirdWidth = thirdWidth,
|
thirdWidth = thirdWidth,
|
||||||
first = {
|
first = {
|
||||||
SidePanel(
|
SidePanel(
|
||||||
tabViewModel.tabViewModelsProvider.sidePanelViewModel,
|
repositoryOpenViewModel.tabViewModelsProvider.sidePanelViewModel,
|
||||||
changeDefaultUpstreamBranchViewModel = { tabViewModel.tabViewModelsProvider.changeDefaultUpstreamBranchViewModel },
|
changeDefaultUpstreamBranchViewModel = { repositoryOpenViewModel.tabViewModelsProvider.changeDefaultUpstreamBranchViewModel },
|
||||||
submoduleDialogViewModel = { tabViewModel.tabViewModelsProvider.submoduleDialogViewModel },
|
submoduleDialogViewModel = { repositoryOpenViewModel.tabViewModelsProvider.submoduleDialogViewModel },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
second = {
|
second = {
|
||||||
@ -328,13 +332,13 @@ fun MainContentView(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
if (rebaseInteractiveState == RebaseInteractiveState.AwaitingInteraction && diffSelected == null) {
|
if (rebaseInteractiveState == RebaseInteractiveState.AwaitingInteraction && diffSelected == null) {
|
||||||
RebaseInteractive(tabViewModel.tabViewModelsProvider.rebaseInteractiveViewModel)
|
RebaseInteractive(repositoryOpenViewModel.tabViewModelsProvider.rebaseInteractiveViewModel)
|
||||||
} else if (blameState is BlameState.Loaded && !blameState.isMinimized) {
|
} else if (blameState is BlameState.Loaded && !blameState.isMinimized) {
|
||||||
Blame(
|
Blame(
|
||||||
filePath = blameState.filePath,
|
filePath = blameState.filePath,
|
||||||
blameResult = blameState.blameResult,
|
blameResult = blameState.blameResult,
|
||||||
onClose = { tabViewModel.resetBlameState() },
|
onClose = { repositoryOpenViewModel.resetBlameState() },
|
||||||
onSelectCommit = { tabViewModel.selectCommit(it) }
|
onSelectCommit = { repositoryOpenViewModel.selectCommit(it) }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Column {
|
Column {
|
||||||
@ -342,21 +346,21 @@ fun MainContentView(
|
|||||||
when (diffSelected) {
|
when (diffSelected) {
|
||||||
null -> {
|
null -> {
|
||||||
Log(
|
Log(
|
||||||
logViewModel = tabViewModel.tabViewModelsProvider.logViewModel,
|
logViewModel = repositoryOpenViewModel.tabViewModelsProvider.logViewModel,
|
||||||
selectedItem = selectedItem,
|
selectedItem = selectedItem,
|
||||||
repositoryState = repositoryState,
|
repositoryState = repositoryState,
|
||||||
changeDefaultUpstreamBranchViewModel = { tabViewModel.tabViewModelsProvider.changeDefaultUpstreamBranchViewModel },
|
changeDefaultUpstreamBranchViewModel = { repositoryOpenViewModel.tabViewModelsProvider.changeDefaultUpstreamBranchViewModel },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val diffViewModel = tabViewModel.diffViewModel
|
val diffViewModel = repositoryOpenViewModel.diffViewModel
|
||||||
|
|
||||||
if (diffViewModel != null) {
|
if (diffViewModel != null) {
|
||||||
Diff(
|
Diff(
|
||||||
diffViewModel = diffViewModel,
|
diffViewModel = diffViewModel,
|
||||||
onCloseDiffView = {
|
onCloseDiffView = {
|
||||||
tabViewModel.newDiffSelected = null
|
repositoryOpenViewModel.newDiffSelected = null
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -367,8 +371,8 @@ fun MainContentView(
|
|||||||
if (blameState is BlameState.Loaded) { // BlameState.isMinimized is true here
|
if (blameState is BlameState.Loaded) { // BlameState.isMinimized is true here
|
||||||
MinimizedBlame(
|
MinimizedBlame(
|
||||||
filePath = blameState.filePath,
|
filePath = blameState.filePath,
|
||||||
onExpand = { tabViewModel.expandBlame() },
|
onExpand = { repositoryOpenViewModel.expandBlame() },
|
||||||
onClose = { tabViewModel.resetBlameState() }
|
onClose = { repositoryOpenViewModel.resetBlameState() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -383,13 +387,13 @@ fun MainContentView(
|
|||||||
when (selectedItem) {
|
when (selectedItem) {
|
||||||
SelectedItem.UncommittedChanges -> {
|
SelectedItem.UncommittedChanges -> {
|
||||||
UncommittedChanges(
|
UncommittedChanges(
|
||||||
statusViewModel = tabViewModel.tabViewModelsProvider.statusViewModel,
|
statusViewModel = repositoryOpenViewModel.tabViewModelsProvider.statusViewModel,
|
||||||
selectedEntryType = diffSelected,
|
selectedEntryType = diffSelected,
|
||||||
repositoryState = repositoryState,
|
repositoryState = repositoryState,
|
||||||
onStagedDiffEntrySelected = { diffEntry ->
|
onStagedDiffEntrySelected = { diffEntry ->
|
||||||
tabViewModel.minimizeBlame()
|
repositoryOpenViewModel.minimizeBlame()
|
||||||
|
|
||||||
tabViewModel.newDiffSelected = if (diffEntry != null) {
|
repositoryOpenViewModel.newDiffSelected = if (diffEntry != null) {
|
||||||
if (repositoryState == RepositoryState.SAFE)
|
if (repositoryState == RepositoryState.SAFE)
|
||||||
DiffType.SafeStagedDiff(diffEntry)
|
DiffType.SafeStagedDiff(diffEntry)
|
||||||
else
|
else
|
||||||
@ -399,29 +403,29 @@ fun MainContentView(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onUnstagedDiffEntrySelected = { diffEntry ->
|
onUnstagedDiffEntrySelected = { diffEntry ->
|
||||||
tabViewModel.minimizeBlame()
|
repositoryOpenViewModel.minimizeBlame()
|
||||||
|
|
||||||
if (repositoryState == RepositoryState.SAFE)
|
if (repositoryState == RepositoryState.SAFE)
|
||||||
tabViewModel.newDiffSelected = DiffType.SafeUnstagedDiff(diffEntry)
|
repositoryOpenViewModel.newDiffSelected = DiffType.SafeUnstagedDiff(diffEntry)
|
||||||
else
|
else
|
||||||
tabViewModel.newDiffSelected = DiffType.UnsafeUnstagedDiff(diffEntry)
|
repositoryOpenViewModel.newDiffSelected = DiffType.UnsafeUnstagedDiff(diffEntry)
|
||||||
},
|
},
|
||||||
onBlameFile = { tabViewModel.blameFile(it) },
|
onBlameFile = { repositoryOpenViewModel.blameFile(it) },
|
||||||
onHistoryFile = { tabViewModel.fileHistory(it) }
|
onHistoryFile = { repositoryOpenViewModel.fileHistory(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is SelectedItem.CommitBasedItem -> {
|
is SelectedItem.CommitBasedItem -> {
|
||||||
CommitChanges(
|
CommitChanges(
|
||||||
commitChangesViewModel = tabViewModel.tabViewModelsProvider.commitChangesViewModel,
|
commitChangesViewModel = repositoryOpenViewModel.tabViewModelsProvider.commitChangesViewModel,
|
||||||
selectedItem = selectedItem,
|
selectedItem = selectedItem,
|
||||||
diffSelected = diffSelected,
|
diffSelected = diffSelected,
|
||||||
onDiffSelected = { diffEntry ->
|
onDiffSelected = { diffEntry ->
|
||||||
tabViewModel.minimizeBlame()
|
repositoryOpenViewModel.minimizeBlame()
|
||||||
tabViewModel.newDiffSelected = DiffType.CommitDiff(diffEntry)
|
repositoryOpenViewModel.newDiffSelected = DiffType.CommitDiff(diffEntry)
|
||||||
},
|
},
|
||||||
onBlame = { tabViewModel.blameFile(it) },
|
onBlame = { repositoryOpenViewModel.blameFile(it) },
|
||||||
onHistory = { tabViewModel.fileHistory(it) },
|
onHistory = { repositoryOpenViewModel.fileHistory(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -431,19 +435,19 @@ fun MainContentView(
|
|||||||
},
|
},
|
||||||
onFirstSizeDragStarted = { currentWidth ->
|
onFirstSizeDragStarted = { currentWidth ->
|
||||||
firstWidth = currentWidth
|
firstWidth = currentWidth
|
||||||
tabViewModel.setFirstPaneWidth(currentWidth)
|
repositoryOpenViewModel.setFirstPaneWidth(currentWidth)
|
||||||
},
|
},
|
||||||
onFirstSizeChange = {
|
onFirstSizeChange = {
|
||||||
val newWidth = firstWidth + it / density
|
val newWidth = firstWidth + it / density
|
||||||
|
|
||||||
if (newWidth > 150 && rebaseInteractiveState !is RebaseInteractiveState.AwaitingInteraction) {
|
if (newWidth > 150 && rebaseInteractiveState !is RebaseInteractiveState.AwaitingInteraction) {
|
||||||
firstWidth = newWidth
|
firstWidth = newWidth
|
||||||
tabViewModel.setFirstPaneWidth(newWidth)
|
repositoryOpenViewModel.setFirstPaneWidth(newWidth)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onFirstSizeDragStopped = {
|
onFirstSizeDragStopped = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
tabViewModel.persistFirstPaneWidth()
|
repositoryOpenViewModel.persistFirstPaneWidth()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onThirdSizeChange = {
|
onThirdSizeChange = {
|
||||||
@ -451,16 +455,16 @@ fun MainContentView(
|
|||||||
|
|
||||||
if (newWidth > 150) {
|
if (newWidth > 150) {
|
||||||
thirdWidth = newWidth
|
thirdWidth = newWidth
|
||||||
tabViewModel.setThirdPaneWidth(newWidth)
|
repositoryOpenViewModel.setThirdPaneWidth(newWidth)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onThirdSizeDragStarted = { currentWidth ->
|
onThirdSizeDragStarted = { currentWidth ->
|
||||||
thirdWidth = currentWidth
|
thirdWidth = currentWidth
|
||||||
tabViewModel.setThirdPaneWidth(currentWidth)
|
repositoryOpenViewModel.setThirdPaneWidth(currentWidth)
|
||||||
},
|
},
|
||||||
onThirdSizeDragStopped = {
|
onThirdSizeDragStopped = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
tabViewModel.persistThirdPaneWidth()
|
repositoryOpenViewModel.persistThirdPaneWidth()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,93 @@
|
|||||||
|
package com.jetpackduba.gitnuro.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.jetpackduba.gitnuro.AppIcons
|
||||||
|
import com.jetpackduba.gitnuro.models.Notification
|
||||||
|
import com.jetpackduba.gitnuro.models.NotificationType
|
||||||
|
import com.jetpackduba.gitnuro.theme.AppTheme
|
||||||
|
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun NotificationSuccessPreview() {
|
||||||
|
AppTheme(customTheme = null) {
|
||||||
|
Notification(NotificationType.Positive, "Hello world!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Notification(notification: Notification) {
|
||||||
|
val notificationShape = RoundedCornerShape(8.dp)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.border(2.dp, MaterialTheme.colors.onBackground.copy(0.2f), notificationShape)
|
||||||
|
.clip(notificationShape)
|
||||||
|
.background(MaterialTheme.colors.background)
|
||||||
|
.height(IntrinsicSize.Max)
|
||||||
|
.padding(2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
val backgroundColor = when (notification.type) {
|
||||||
|
NotificationType.Positive -> MaterialTheme.colors.primary
|
||||||
|
NotificationType.Warning -> MaterialTheme.colors.secondary
|
||||||
|
NotificationType.Error -> MaterialTheme.colors.error
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentColor = when (notification.type) {
|
||||||
|
NotificationType.Positive -> MaterialTheme.colors.onPrimary
|
||||||
|
NotificationType.Warning -> MaterialTheme.colors.onSecondary
|
||||||
|
NotificationType.Error -> MaterialTheme.colors.onError
|
||||||
|
}
|
||||||
|
|
||||||
|
val icon = when (notification.type) {
|
||||||
|
NotificationType.Positive -> AppIcons.INFO
|
||||||
|
NotificationType.Warning -> AppIcons.WARNING
|
||||||
|
NotificationType.Error -> AppIcons.ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(topStart = 6.dp, bottomStart = 6.dp))
|
||||||
|
.background(backgroundColor)
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(icon),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = contentColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
.fillMaxHeight(),
|
||||||
|
contentAlignment = Alignment.CenterStart,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = notification.text,
|
||||||
|
modifier = Modifier,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
style = MaterialTheme.typography.body1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package com.jetpackduba.gitnuro.ui.dialogs
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import com.jetpackduba.gitnuro.credentials.CredentialsAccepted
|
||||||
|
import com.jetpackduba.gitnuro.credentials.CredentialsRequested
|
||||||
|
import com.jetpackduba.gitnuro.credentials.CredentialsState
|
||||||
|
import com.jetpackduba.gitnuro.viewmodels.TabViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CredentialsDialog(gitManager: TabViewModel) {
|
||||||
|
val credentialsState = gitManager.credentialsState.collectAsState()
|
||||||
|
|
||||||
|
when (val credentialsStateValue = credentialsState.value) {
|
||||||
|
CredentialsRequested.HttpCredentialsRequested -> {
|
||||||
|
UserPasswordDialog(
|
||||||
|
onReject = {
|
||||||
|
gitManager.credentialsDenied()
|
||||||
|
},
|
||||||
|
onAccept = { user, password ->
|
||||||
|
gitManager.httpCredentialsAccepted(user, password)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CredentialsRequested.SshCredentialsRequested -> {
|
||||||
|
SshPasswordDialog(
|
||||||
|
onReject = {
|
||||||
|
gitManager.credentialsDenied()
|
||||||
|
},
|
||||||
|
onAccept = { password ->
|
||||||
|
gitManager.sshCredentialsAccepted(password)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is CredentialsRequested.GpgCredentialsRequested -> {
|
||||||
|
GpgPasswordDialog(
|
||||||
|
gpgCredentialsRequested = credentialsStateValue,
|
||||||
|
onReject = {
|
||||||
|
gitManager.credentialsDenied()
|
||||||
|
},
|
||||||
|
onAccept = { password ->
|
||||||
|
gitManager.gpgCredentialsAccepted(password)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is CredentialsAccepted, CredentialsState.None, CredentialsState.CredentialsDenied -> { /* Nothing to do */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,7 @@ private val updateJson = Json {
|
|||||||
class UpdatesRepository @Inject constructor(
|
class UpdatesRepository @Inject constructor(
|
||||||
private val updatesWebService: UpdatesService,
|
private val updatesWebService: UpdatesService,
|
||||||
) {
|
) {
|
||||||
fun hasUpdatesFlow() = flow {
|
val hasUpdatesFlow = flow {
|
||||||
val latestReleaseJson = updatesWebService.release(AppConstants.VERSION_CHECK_URL)
|
val latestReleaseJson = updatesWebService.release(AppConstants.VERSION_CHECK_URL)
|
||||||
|
|
||||||
while (coroutineContext.isActive) {
|
while (coroutineContext.isActive) {
|
||||||
|
@ -0,0 +1,380 @@
|
|||||||
|
package com.jetpackduba.gitnuro.viewmodels
|
||||||
|
|
||||||
|
import com.jetpackduba.gitnuro.SharedRepositoryStateManager
|
||||||
|
import com.jetpackduba.gitnuro.TaskType
|
||||||
|
import com.jetpackduba.gitnuro.credentials.CredentialsAccepted
|
||||||
|
import com.jetpackduba.gitnuro.credentials.CredentialsState
|
||||||
|
import com.jetpackduba.gitnuro.credentials.CredentialsStateManager
|
||||||
|
import com.jetpackduba.gitnuro.exceptions.WatcherInitException
|
||||||
|
import com.jetpackduba.gitnuro.git.*
|
||||||
|
import com.jetpackduba.gitnuro.git.branches.CreateBranchUseCase
|
||||||
|
import com.jetpackduba.gitnuro.git.rebase.RebaseInteractiveState
|
||||||
|
import com.jetpackduba.gitnuro.git.repository.InitLocalRepositoryUseCase
|
||||||
|
import com.jetpackduba.gitnuro.git.repository.OpenRepositoryUseCase
|
||||||
|
import com.jetpackduba.gitnuro.git.repository.OpenSubmoduleRepositoryUseCase
|
||||||
|
import com.jetpackduba.gitnuro.git.stash.StashChangesUseCase
|
||||||
|
import com.jetpackduba.gitnuro.git.workspace.StageUntrackedFileUseCase
|
||||||
|
import com.jetpackduba.gitnuro.logging.printDebug
|
||||||
|
import com.jetpackduba.gitnuro.logging.printLog
|
||||||
|
import com.jetpackduba.gitnuro.managers.AppStateManager
|
||||||
|
import com.jetpackduba.gitnuro.managers.ErrorsManager
|
||||||
|
import com.jetpackduba.gitnuro.managers.newErrorNow
|
||||||
|
import com.jetpackduba.gitnuro.models.AuthorInfoSimple
|
||||||
|
import com.jetpackduba.gitnuro.models.errorNotification
|
||||||
|
import com.jetpackduba.gitnuro.models.positiveNotification
|
||||||
|
import com.jetpackduba.gitnuro.system.OpenFilePickerUseCase
|
||||||
|
import com.jetpackduba.gitnuro.system.OpenUrlInBrowserUseCase
|
||||||
|
import com.jetpackduba.gitnuro.system.PickerType
|
||||||
|
import com.jetpackduba.gitnuro.ui.IVerticalSplitPaneConfig
|
||||||
|
import com.jetpackduba.gitnuro.ui.SelectedItem
|
||||||
|
import com.jetpackduba.gitnuro.ui.TabsManager
|
||||||
|
import com.jetpackduba.gitnuro.ui.VerticalSplitPaneConfig
|
||||||
|
import com.jetpackduba.gitnuro.updates.Update
|
||||||
|
import com.jetpackduba.gitnuro.updates.UpdatesRepository
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.eclipse.jgit.api.errors.CheckoutConflictException
|
||||||
|
import org.eclipse.jgit.blame.BlameResult
|
||||||
|
import org.eclipse.jgit.lib.Repository
|
||||||
|
import org.eclipse.jgit.lib.RepositoryState
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit
|
||||||
|
import java.awt.Desktop
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Provider
|
||||||
|
|
||||||
|
private const val MIN_TIME_AFTER_GIT_OPERATION = 2000L
|
||||||
|
|
||||||
|
private const val TAG = "TabViewModel"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 RepositoryOpenViewModel @Inject constructor(
|
||||||
|
private val getWorkspacePathUseCase: GetWorkspacePathUseCase,
|
||||||
|
private val diffViewModelProvider: Provider<DiffViewModel>,
|
||||||
|
private val historyViewModelProvider: Provider<HistoryViewModel>,
|
||||||
|
private val authorViewModelProvider: Provider<AuthorViewModel>,
|
||||||
|
private val tabState: TabState,
|
||||||
|
val appStateManager: AppStateManager,
|
||||||
|
private val fileChangesWatcher: FileChangesWatcher,
|
||||||
|
private val createBranchUseCase: CreateBranchUseCase,
|
||||||
|
private val stashChangesUseCase: StashChangesUseCase,
|
||||||
|
private val stageUntrackedFileUseCase: StageUntrackedFileUseCase,
|
||||||
|
private val openFilePickerUseCase: OpenFilePickerUseCase,
|
||||||
|
private val openUrlInBrowserUseCase: OpenUrlInBrowserUseCase,
|
||||||
|
private val tabsManager: TabsManager,
|
||||||
|
private val tabScope: CoroutineScope,
|
||||||
|
private val verticalSplitPaneConfig: VerticalSplitPaneConfig,
|
||||||
|
val tabViewModelsProvider: TabViewModelsProvider,
|
||||||
|
private val globalMenuActionsViewModel: GlobalMenuActionsViewModel,
|
||||||
|
sharedRepositoryStateManager: SharedRepositoryStateManager,
|
||||||
|
updatesRepository: UpdatesRepository,
|
||||||
|
) : IVerticalSplitPaneConfig by verticalSplitPaneConfig,
|
||||||
|
IGlobalMenuActionsViewModel by globalMenuActionsViewModel {
|
||||||
|
private val errorsManager: ErrorsManager = tabState.errorsManager
|
||||||
|
|
||||||
|
val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem
|
||||||
|
var diffViewModel: DiffViewModel? = null
|
||||||
|
|
||||||
|
val repositoryState: StateFlow<RepositoryState> = sharedRepositoryStateManager.repositoryState
|
||||||
|
val rebaseInteractiveState: StateFlow<RebaseInteractiveState> = sharedRepositoryStateManager.rebaseInteractiveState
|
||||||
|
|
||||||
|
private val _diffSelected = MutableStateFlow<DiffType?>(null)
|
||||||
|
val diffSelected: StateFlow<DiffType?> = _diffSelected
|
||||||
|
|
||||||
|
var newDiffSelected: DiffType?
|
||||||
|
get() = diffSelected.value
|
||||||
|
set(value) {
|
||||||
|
_diffSelected.value = value
|
||||||
|
updateDiffEntry()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _blameState = MutableStateFlow<BlameState>(BlameState.None)
|
||||||
|
val blameState: StateFlow<BlameState> = _blameState
|
||||||
|
|
||||||
|
private val _showHistory = MutableStateFlow(false)
|
||||||
|
val showHistory: StateFlow<Boolean> = _showHistory
|
||||||
|
|
||||||
|
private val _showAuthorInfo = MutableStateFlow(false)
|
||||||
|
val showAuthorInfo: StateFlow<Boolean> = _showAuthorInfo
|
||||||
|
|
||||||
|
private val _authorInfoSimple = MutableStateFlow(AuthorInfoSimple(null, null))
|
||||||
|
val authorInfoSimple: StateFlow<AuthorInfoSimple> = _authorInfoSimple
|
||||||
|
|
||||||
|
var historyViewModel: HistoryViewModel? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
var authorViewModel: AuthorViewModel? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
init {
|
||||||
|
tabScope.run {
|
||||||
|
launch {
|
||||||
|
tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REPO_STATE) {
|
||||||
|
loadAuthorInfo(tabState.git)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
watchRepositoryChanges(tabState.git)
|
||||||
|
}
|
||||||
|
|
||||||
|
launch { tabState.refreshData(RefreshType.ALL_DATA) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To make sure the tab opens the new repository with a clean state,
|
||||||
|
* instead of opening the repo in the same ViewModel we simply create a new tab with a new TabViewModel
|
||||||
|
* replacing the current tab
|
||||||
|
*/
|
||||||
|
fun openAnotherRepository(directory: String) = tabState.runOperation(
|
||||||
|
showError = true,
|
||||||
|
refreshType = RefreshType.NONE,
|
||||||
|
) { git ->
|
||||||
|
tabsManager.addNewTabFromPath(directory, true, getWorkspacePathUseCase(git))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadAuthorInfo(git: Git) {
|
||||||
|
val config = git.repository.config
|
||||||
|
config.load()
|
||||||
|
val userName = config.getString("user", null, "name")
|
||||||
|
val email = config.getString("user", null, "email")
|
||||||
|
|
||||||
|
_authorInfoSimple.value = AuthorInfoSimple(userName, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAuthorInfoDialog() {
|
||||||
|
authorViewModel = authorViewModelProvider.get()
|
||||||
|
authorViewModel?.loadAuthorInfo()
|
||||||
|
_showAuthorInfo.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeAuthorInfoDialog() {
|
||||||
|
_showAuthorInfo.value = false
|
||||||
|
authorViewModel = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sometimes external apps can run filesystem multiple operations in a fraction of a second.
|
||||||
|
* To prevent excessive updates, we add a slight delay between updates emission to prevent slowing down
|
||||||
|
* the app by constantly running "git status" or even full refreshes.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private suspend fun watchRepositoryChanges(git: Git) = tabScope.launch(Dispatchers.IO) {
|
||||||
|
var hasGitDirChanged = false
|
||||||
|
|
||||||
|
launch {
|
||||||
|
fileChangesWatcher.changesNotifier.collect { latestUpdateChangedGitDir ->
|
||||||
|
val isOperationRunning = tabState.operationRunning
|
||||||
|
|
||||||
|
if (!isOperationRunning) { // Only update if there isn't any process running
|
||||||
|
printDebug(TAG, "Detected changes in the repository's directory")
|
||||||
|
|
||||||
|
val currentTimeMillis = System.currentTimeMillis()
|
||||||
|
|
||||||
|
if (
|
||||||
|
latestUpdateChangedGitDir &&
|
||||||
|
currentTimeMillis - tabState.lastOperation < MIN_TIME_AFTER_GIT_OPERATION
|
||||||
|
) {
|
||||||
|
printDebug(TAG, "Git operation was executed recently, ignoring file system change")
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestUpdateChangedGitDir) {
|
||||||
|
hasGitDirChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
updateApp(hasGitDirChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasGitDirChanged = false
|
||||||
|
} else {
|
||||||
|
printDebug(TAG, "Ignored file events during operation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fileChangesWatcher.watchDirectoryPath(
|
||||||
|
repository = git.repository,
|
||||||
|
)
|
||||||
|
} catch (ex: WatcherInitException) {
|
||||||
|
val message = ex.message
|
||||||
|
if (message != null) {
|
||||||
|
errorsManager.addError(
|
||||||
|
newErrorNow(
|
||||||
|
exception = ex,
|
||||||
|
taskType = TaskType.CHANGES_DETECTION,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateApp(hasGitDirChanged: Boolean) {
|
||||||
|
if (hasGitDirChanged) {
|
||||||
|
printLog(TAG, "Changes detected in git directory, full refresh")
|
||||||
|
|
||||||
|
refreshRepositoryInfo()
|
||||||
|
} else {
|
||||||
|
printLog(TAG, "Changes detected, partial refresh")
|
||||||
|
|
||||||
|
checkUncommittedChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun checkUncommittedChanges() = tabState.runOperation(
|
||||||
|
refreshType = RefreshType.NONE,
|
||||||
|
) {
|
||||||
|
updateDiffEntry()
|
||||||
|
tabState.refreshData(RefreshType.UNCOMMITTED_CHANGES_AND_LOG)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshRepositoryInfo() {
|
||||||
|
tabState.refreshData(RefreshType.ALL_DATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDiffEntry() {
|
||||||
|
val diffSelected = diffSelected.value
|
||||||
|
|
||||||
|
if (diffSelected != null) {
|
||||||
|
if (diffViewModel == null) { // Initialize the view model if required
|
||||||
|
diffViewModel = diffViewModelProvider.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
diffViewModel?.cancelRunningJobs()
|
||||||
|
diffViewModel?.updateDiff(diffSelected)
|
||||||
|
} else {
|
||||||
|
diffViewModel?.cancelRunningJobs()
|
||||||
|
diffViewModel = null // Free the view model from the memory if not being used.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openDirectoryPicker(): String? {
|
||||||
|
val latestDirectoryOpened = appStateManager.latestOpenedRepositoryPath
|
||||||
|
|
||||||
|
return openFilePickerUseCase(PickerType.DIRECTORIES, latestDirectoryOpened)
|
||||||
|
}
|
||||||
|
|
||||||
|
val update: StateFlow<Update?> = updatesRepository.hasUpdatesFlow
|
||||||
|
.stateIn(tabScope, started = SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
|
fun blameFile(filePath: String) = tabState.safeProcessing(
|
||||||
|
refreshType = RefreshType.NONE,
|
||||||
|
taskType = TaskType.BLAME_FILE,
|
||||||
|
) { git ->
|
||||||
|
_blameState.value = BlameState.Loading(filePath)
|
||||||
|
try {
|
||||||
|
val result = git.blame()
|
||||||
|
.setFilePath(filePath)
|
||||||
|
.setFollowFileRenames(true)
|
||||||
|
.call() ?: throw Exception("File is no longer present in the workspace and can't be blamed")
|
||||||
|
|
||||||
|
_blameState.value = BlameState.Loaded(filePath, result)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
resetBlameState()
|
||||||
|
|
||||||
|
throw ex
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetBlameState() {
|
||||||
|
_blameState.value = BlameState.None
|
||||||
|
}
|
||||||
|
|
||||||
|
fun expandBlame() {
|
||||||
|
val blameState = _blameState.value
|
||||||
|
|
||||||
|
if (blameState is BlameState.Loaded && blameState.isMinimized) {
|
||||||
|
_blameState.value = blameState.copy(isMinimized = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun minimizeBlame() {
|
||||||
|
val blameState = _blameState.value
|
||||||
|
|
||||||
|
if (blameState is BlameState.Loaded && !blameState.isMinimized) {
|
||||||
|
_blameState.value = blameState.copy(isMinimized = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectCommit(commit: RevCommit) = tabState.runOperation(
|
||||||
|
refreshType = RefreshType.NONE,
|
||||||
|
) {
|
||||||
|
tabState.newSelectedCommit(commit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fileHistory(filePath: String) {
|
||||||
|
historyViewModel = historyViewModelProvider.get()
|
||||||
|
historyViewModel?.fileHistory(filePath)
|
||||||
|
_showHistory.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeHistory() {
|
||||||
|
_showHistory.value = false
|
||||||
|
historyViewModel = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshAll() = tabScope.launch {
|
||||||
|
printLog(TAG, "Manual refresh triggered. IS OPERATION RUNNING ${tabState.operationRunning}")
|
||||||
|
if (!tabState.operationRunning) {
|
||||||
|
refreshRepositoryInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createBranch(branchName: String) = tabState.safeProcessing(
|
||||||
|
refreshType = RefreshType.ALL_DATA,
|
||||||
|
refreshEvenIfCrashesInteractive = { it is CheckoutConflictException },
|
||||||
|
taskType = TaskType.CREATE_BRANCH,
|
||||||
|
) { git ->
|
||||||
|
createBranchUseCase(git, branchName)
|
||||||
|
|
||||||
|
positiveNotification("Branch \"${branchName}\" created")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stashWithMessage(message: String) = tabState.safeProcessing(
|
||||||
|
refreshType = RefreshType.UNCOMMITTED_CHANGES_AND_LOG,
|
||||||
|
taskType = TaskType.STASH,
|
||||||
|
) { git ->
|
||||||
|
stageUntrackedFileUseCase(git)
|
||||||
|
|
||||||
|
if (stashChangesUseCase(git, message)) {
|
||||||
|
positiveNotification("Changes stashed")
|
||||||
|
} else {
|
||||||
|
errorNotification("There are no changes to stash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openFolderInFileExplorer() = tabState.runOperation(
|
||||||
|
showError = true,
|
||||||
|
refreshType = RefreshType.NONE,
|
||||||
|
) { git ->
|
||||||
|
Desktop.getDesktop().open(git.repository.workTree)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openUrlInBrowser(url: String) {
|
||||||
|
openUrlInBrowserUseCase(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeLastView() = tabScope.launch {
|
||||||
|
tabState.closeLastView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sealed interface BlameState {
|
||||||
|
data class Loading(val filePath: String) : BlameState
|
||||||
|
|
||||||
|
data class Loaded(val filePath: String, val blameResult: BlameResult, val isMinimized: Boolean = false) : BlameState
|
||||||
|
|
||||||
|
data object None : BlameState
|
||||||
|
}
|
@ -57,81 +57,39 @@ class TabViewModel @Inject constructor(
|
|||||||
private val initLocalRepositoryUseCase: InitLocalRepositoryUseCase,
|
private val initLocalRepositoryUseCase: InitLocalRepositoryUseCase,
|
||||||
private val openRepositoryUseCase: OpenRepositoryUseCase,
|
private val openRepositoryUseCase: OpenRepositoryUseCase,
|
||||||
private val openSubmoduleRepositoryUseCase: OpenSubmoduleRepositoryUseCase,
|
private val openSubmoduleRepositoryUseCase: OpenSubmoduleRepositoryUseCase,
|
||||||
private val getWorkspacePathUseCase: GetWorkspacePathUseCase,
|
|
||||||
private val diffViewModelProvider: Provider<DiffViewModel>,
|
|
||||||
private val historyViewModelProvider: Provider<HistoryViewModel>,
|
|
||||||
private val authorViewModelProvider: Provider<AuthorViewModel>,
|
|
||||||
private val tabState: TabState,
|
private val tabState: TabState,
|
||||||
val appStateManager: AppStateManager,
|
val appStateManager: AppStateManager,
|
||||||
private val fileChangesWatcher: FileChangesWatcher,
|
private val fileChangesWatcher: FileChangesWatcher,
|
||||||
private val updatesRepository: UpdatesRepository,
|
|
||||||
private val credentialsStateManager: CredentialsStateManager,
|
private val credentialsStateManager: CredentialsStateManager,
|
||||||
private val createBranchUseCase: CreateBranchUseCase,
|
|
||||||
private val stashChangesUseCase: StashChangesUseCase,
|
|
||||||
private val stageUntrackedFileUseCase: StageUntrackedFileUseCase,
|
|
||||||
private val openFilePickerUseCase: OpenFilePickerUseCase,
|
private val openFilePickerUseCase: OpenFilePickerUseCase,
|
||||||
private val openUrlInBrowserUseCase: OpenUrlInBrowserUseCase,
|
private val openUrlInBrowserUseCase: OpenUrlInBrowserUseCase,
|
||||||
private val sharedRepositoryStateManager: SharedRepositoryStateManager,
|
|
||||||
private val tabsManager: TabsManager,
|
|
||||||
private val tabScope: CoroutineScope,
|
private val tabScope: CoroutineScope,
|
||||||
private val verticalSplitPaneConfig: VerticalSplitPaneConfig,
|
private val verticalSplitPaneConfig: VerticalSplitPaneConfig,
|
||||||
val tabViewModelsProvider: TabViewModelsProvider,
|
val tabViewModelsProvider: TabViewModelsProvider,
|
||||||
private val globalMenuActionsViewModel: GlobalMenuActionsViewModel,
|
private val globalMenuActionsViewModel: GlobalMenuActionsViewModel,
|
||||||
|
private val repositoryOpenViewModelProvider: Provider<RepositoryOpenViewModel>,
|
||||||
|
updatesRepository: UpdatesRepository,
|
||||||
) : IVerticalSplitPaneConfig by verticalSplitPaneConfig,
|
) : IVerticalSplitPaneConfig by verticalSplitPaneConfig,
|
||||||
IGlobalMenuActionsViewModel by globalMenuActionsViewModel {
|
IGlobalMenuActionsViewModel by globalMenuActionsViewModel {
|
||||||
var initialPath: String? = null // Stores the path that should be opened when the tab is selected
|
var initialPath: String? = null // Stores the path that should be opened when the tab is selected
|
||||||
val errorsManager: ErrorsManager = tabState.errorsManager
|
val errorsManager: ErrorsManager = tabState.errorsManager
|
||||||
val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem
|
val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem
|
||||||
var diffViewModel: DiffViewModel? = null
|
|
||||||
|
|
||||||
|
val repositoryOpenViewModel: RepositoryOpenViewModel by lazy {
|
||||||
|
repositoryOpenViewModelProvider.get()
|
||||||
|
}
|
||||||
private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(RepositorySelectionStatus.None)
|
private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(RepositorySelectionStatus.None)
|
||||||
val repositorySelectionStatus: StateFlow<RepositorySelectionStatus>
|
val repositorySelectionStatus: StateFlow<RepositorySelectionStatus>
|
||||||
get() = _repositorySelectionStatus
|
get() = _repositorySelectionStatus
|
||||||
|
|
||||||
val repositoryState: StateFlow<RepositoryState> = sharedRepositoryStateManager.repositoryState
|
|
||||||
val rebaseInteractiveState: StateFlow<RebaseInteractiveState> = sharedRepositoryStateManager.rebaseInteractiveState
|
|
||||||
|
|
||||||
val processing: StateFlow<ProcessingState> = tabState.processing
|
val processing: StateFlow<ProcessingState> = tabState.processing
|
||||||
|
|
||||||
val credentialsState: StateFlow<CredentialsState> = credentialsStateManager.credentialsState
|
val credentialsState: StateFlow<CredentialsState> = credentialsStateManager.credentialsState
|
||||||
|
|
||||||
private val _diffSelected = MutableStateFlow<DiffType?>(null)
|
|
||||||
val diffSelected: StateFlow<DiffType?> = _diffSelected
|
|
||||||
|
|
||||||
var newDiffSelected: DiffType?
|
|
||||||
get() = diffSelected.value
|
|
||||||
set(value) {
|
|
||||||
_diffSelected.value = value
|
|
||||||
updateDiffEntry()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _blameState = MutableStateFlow<BlameState>(BlameState.None)
|
|
||||||
val blameState: StateFlow<BlameState> = _blameState
|
|
||||||
|
|
||||||
private val _showHistory = MutableStateFlow(false)
|
|
||||||
val showHistory: StateFlow<Boolean> = _showHistory
|
|
||||||
|
|
||||||
private val _showAuthorInfo = MutableStateFlow(false)
|
|
||||||
val showAuthorInfo: StateFlow<Boolean> = _showAuthorInfo
|
|
||||||
|
|
||||||
private val _authorInfoSimple = MutableStateFlow(AuthorInfoSimple(null, null))
|
|
||||||
val authorInfoSimple: StateFlow<AuthorInfoSimple> = _authorInfoSimple
|
|
||||||
|
|
||||||
var historyViewModel: HistoryViewModel? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
var authorViewModel: AuthorViewModel? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
val showError = MutableStateFlow(false)
|
val showError = MutableStateFlow(false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
tabScope.run {
|
tabScope.run {
|
||||||
launch {
|
|
||||||
tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REPO_STATE) {
|
|
||||||
loadAuthorInfo(tabState.git)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
errorsManager.error.collect {
|
errorsManager.error.collect {
|
||||||
@ -141,19 +99,6 @@ class TabViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To make sure the tab opens the new repository with a clean state,
|
|
||||||
* instead of opening the repo in the same ViewModel we simply create a new tab with a new TabViewModel
|
|
||||||
* replacing the current tab
|
|
||||||
*/
|
|
||||||
fun openAnotherRepository(directory: String) = tabState.runOperation(
|
|
||||||
showError = true,
|
|
||||||
refreshType = RefreshType.NONE,
|
|
||||||
) { git ->
|
|
||||||
tabsManager.addNewTabFromPath(directory, true, getWorkspacePathUseCase(git))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openRepository(directory: String) {
|
fun openRepository(directory: String) {
|
||||||
openRepository(File(directory))
|
openRepository(File(directory))
|
||||||
}
|
}
|
||||||
@ -182,10 +127,6 @@ class TabViewModel @Inject constructor(
|
|||||||
|
|
||||||
onRepositoryChanged(path)
|
onRepositoryChanged(path)
|
||||||
tabState.newSelectedItem(selectedItem = SelectedItem.UncommittedChanges)
|
tabState.newSelectedItem(selectedItem = SelectedItem.UncommittedChanges)
|
||||||
newDiffSelected = null
|
|
||||||
refreshRepositoryInfo()
|
|
||||||
|
|
||||||
watchRepositoryChanges(git)
|
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
onRepositoryChanged(null)
|
onRepositoryChanged(null)
|
||||||
ex.printStackTrace()
|
ex.printStackTrace()
|
||||||
@ -200,109 +141,6 @@ class TabViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadAuthorInfo(git: Git) {
|
|
||||||
val config = git.repository.config
|
|
||||||
config.load()
|
|
||||||
val userName = config.getString("user", null, "name")
|
|
||||||
val email = config.getString("user", null, "email")
|
|
||||||
|
|
||||||
_authorInfoSimple.value = AuthorInfoSimple(userName, email)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showAuthorInfoDialog() {
|
|
||||||
authorViewModel = authorViewModelProvider.get()
|
|
||||||
authorViewModel?.loadAuthorInfo()
|
|
||||||
_showAuthorInfo.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun closeAuthorInfoDialog() {
|
|
||||||
_showAuthorInfo.value = false
|
|
||||||
authorViewModel = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sometimes external apps can run filesystem multiple operations in a fraction of a second.
|
|
||||||
* To prevent excessive updates, we add a slight delay between updates emission to prevent slowing down
|
|
||||||
* the app by constantly running "git status" or even full refreshes.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
private suspend fun watchRepositoryChanges(git: Git) = tabScope.launch(Dispatchers.IO) {
|
|
||||||
var hasGitDirChanged = false
|
|
||||||
|
|
||||||
launch {
|
|
||||||
fileChangesWatcher.changesNotifier.collect { latestUpdateChangedGitDir ->
|
|
||||||
val isOperationRunning = tabState.operationRunning
|
|
||||||
|
|
||||||
if (!isOperationRunning) { // Only update if there isn't any process running
|
|
||||||
printDebug(TAG, "Detected changes in the repository's directory")
|
|
||||||
|
|
||||||
val currentTimeMillis = System.currentTimeMillis()
|
|
||||||
|
|
||||||
if (
|
|
||||||
latestUpdateChangedGitDir &&
|
|
||||||
currentTimeMillis - tabState.lastOperation < MIN_TIME_AFTER_GIT_OPERATION
|
|
||||||
) {
|
|
||||||
printDebug(TAG, "Git operation was executed recently, ignoring file system change")
|
|
||||||
return@collect
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latestUpdateChangedGitDir) {
|
|
||||||
hasGitDirChanged = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
updateApp(hasGitDirChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasGitDirChanged = false
|
|
||||||
} else {
|
|
||||||
printDebug(TAG, "Ignored file events during operation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
fileChangesWatcher.watchDirectoryPath(
|
|
||||||
repository = git.repository,
|
|
||||||
)
|
|
||||||
} catch (ex: WatcherInitException) {
|
|
||||||
val message = ex.message
|
|
||||||
if (message != null) {
|
|
||||||
errorsManager.addError(
|
|
||||||
newErrorNow(
|
|
||||||
exception = ex,
|
|
||||||
taskType = TaskType.CHANGES_DETECTION,
|
|
||||||
// message = message,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateApp(hasGitDirChanged: Boolean) {
|
|
||||||
if (hasGitDirChanged) {
|
|
||||||
printLog(TAG, "Changes detected in git directory, full refresh")
|
|
||||||
|
|
||||||
refreshRepositoryInfo()
|
|
||||||
} else {
|
|
||||||
printLog(TAG, "Changes detected, partial refresh")
|
|
||||||
|
|
||||||
checkUncommittedChanges()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun checkUncommittedChanges() = tabState.runOperation(
|
|
||||||
refreshType = RefreshType.NONE,
|
|
||||||
) {
|
|
||||||
updateDiffEntry()
|
|
||||||
tabState.refreshData(RefreshType.UNCOMMITTED_CHANGES_AND_LOG)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun refreshRepositoryInfo() {
|
|
||||||
tabState.refreshData(RefreshType.ALL_DATA)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun credentialsDenied() {
|
fun credentialsDenied() {
|
||||||
credentialsStateManager.updateState(CredentialsState.CredentialsDenied)
|
credentialsStateManager.updateState(CredentialsState.CredentialsDenied)
|
||||||
}
|
}
|
||||||
@ -322,22 +160,6 @@ class TabViewModel @Inject constructor(
|
|||||||
tabScope.cancel()
|
tabScope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateDiffEntry() {
|
|
||||||
val diffSelected = diffSelected.value
|
|
||||||
|
|
||||||
if (diffSelected != null) {
|
|
||||||
if (diffViewModel == null) { // Initialize the view model if required
|
|
||||||
diffViewModel = diffViewModelProvider.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
diffViewModel?.cancelRunningJobs()
|
|
||||||
diffViewModel?.updateDiff(diffSelected)
|
|
||||||
} else {
|
|
||||||
diffViewModel?.cancelRunningJobs()
|
|
||||||
diffViewModel = null // Free the view model from the memory if not being used.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openDirectoryPicker(): String? {
|
fun openDirectoryPicker(): String? {
|
||||||
val latestDirectoryOpened = appStateManager.latestOpenedRepositoryPath
|
val latestDirectoryOpened = appStateManager.latestOpenedRepositoryPath
|
||||||
|
|
||||||
@ -350,105 +172,9 @@ class TabViewModel @Inject constructor(
|
|||||||
openRepository(repoDir)
|
openRepository(repoDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
val update: StateFlow<Update?> = updatesRepository.hasUpdatesFlow()
|
val update: StateFlow<Update?> = updatesRepository.hasUpdatesFlow
|
||||||
.flowOn(Dispatchers.IO)
|
|
||||||
.stateIn(tabScope, started = SharingStarted.Eagerly, null)
|
.stateIn(tabScope, started = SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
fun blameFile(filePath: String) = tabState.safeProcessing(
|
|
||||||
refreshType = RefreshType.NONE,
|
|
||||||
taskType = TaskType.BLAME_FILE,
|
|
||||||
) { git ->
|
|
||||||
_blameState.value = BlameState.Loading(filePath)
|
|
||||||
try {
|
|
||||||
val result = git.blame()
|
|
||||||
.setFilePath(filePath)
|
|
||||||
.setFollowFileRenames(true)
|
|
||||||
.call() ?: throw Exception("File is no longer present in the workspace and can't be blamed")
|
|
||||||
|
|
||||||
_blameState.value = BlameState.Loaded(filePath, result)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
resetBlameState()
|
|
||||||
|
|
||||||
throw ex
|
|
||||||
}
|
|
||||||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resetBlameState() {
|
|
||||||
_blameState.value = BlameState.None
|
|
||||||
}
|
|
||||||
|
|
||||||
fun expandBlame() {
|
|
||||||
val blameState = _blameState.value
|
|
||||||
|
|
||||||
if (blameState is BlameState.Loaded && blameState.isMinimized) {
|
|
||||||
_blameState.value = blameState.copy(isMinimized = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun minimizeBlame() {
|
|
||||||
val blameState = _blameState.value
|
|
||||||
|
|
||||||
if (blameState is BlameState.Loaded && !blameState.isMinimized) {
|
|
||||||
_blameState.value = blameState.copy(isMinimized = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun selectCommit(commit: RevCommit) = tabState.runOperation(
|
|
||||||
refreshType = RefreshType.NONE,
|
|
||||||
) {
|
|
||||||
tabState.newSelectedCommit(commit)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fileHistory(filePath: String) {
|
|
||||||
historyViewModel = historyViewModelProvider.get()
|
|
||||||
historyViewModel?.fileHistory(filePath)
|
|
||||||
_showHistory.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun closeHistory() {
|
|
||||||
_showHistory.value = false
|
|
||||||
historyViewModel = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshAll() = tabScope.launch {
|
|
||||||
printLog(TAG, "Manual refresh triggered. IS OPERATION RUNNING ${tabState.operationRunning}")
|
|
||||||
if (!tabState.operationRunning) {
|
|
||||||
refreshRepositoryInfo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createBranch(branchName: String) = tabState.safeProcessing(
|
|
||||||
refreshType = RefreshType.ALL_DATA,
|
|
||||||
refreshEvenIfCrashesInteractive = { it is CheckoutConflictException },
|
|
||||||
taskType = TaskType.CREATE_BRANCH,
|
|
||||||
) { git ->
|
|
||||||
createBranchUseCase(git, branchName)
|
|
||||||
|
|
||||||
positiveNotification("Branch \"${branchName}\" created")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stashWithMessage(message: String) = tabState.safeProcessing(
|
|
||||||
refreshType = RefreshType.UNCOMMITTED_CHANGES_AND_LOG,
|
|
||||||
taskType = TaskType.STASH,
|
|
||||||
) { git ->
|
|
||||||
stageUntrackedFileUseCase(git)
|
|
||||||
|
|
||||||
if (stashChangesUseCase(git, message)) {
|
|
||||||
positiveNotification("Changes stashed")
|
|
||||||
} else {
|
|
||||||
errorNotification("There are no changes to stash")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openFolderInFileExplorer() = tabState.runOperation(
|
|
||||||
showError = true,
|
|
||||||
refreshType = RefreshType.NONE,
|
|
||||||
) { git ->
|
|
||||||
Desktop.getDesktop().open(git.repository.workTree)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun gpgCredentialsAccepted(password: String) {
|
fun gpgCredentialsAccepted(password: String) {
|
||||||
credentialsStateManager.updateState(CredentialsAccepted.GpgCredentialsAccepted(password))
|
credentialsStateManager.updateState(CredentialsAccepted.GpgCredentialsAccepted(password))
|
||||||
}
|
}
|
||||||
@ -468,15 +194,7 @@ class TabViewModel @Inject constructor(
|
|||||||
|
|
||||||
|
|
||||||
sealed class RepositorySelectionStatus {
|
sealed class RepositorySelectionStatus {
|
||||||
object None : RepositorySelectionStatus()
|
data object None : RepositorySelectionStatus()
|
||||||
data class Opening(val path: String) : RepositorySelectionStatus()
|
data class Opening(val path: String) : RepositorySelectionStatus()
|
||||||
data class Open(val repository: Repository) : RepositorySelectionStatus()
|
data class Open(val repository: Repository) : RepositorySelectionStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface BlameState {
|
|
||||||
data class Loading(val filePath: String) : BlameState
|
|
||||||
|
|
||||||
data class Loaded(val filePath: String, val blameResult: BlameResult, val isMinimized: Boolean = false) : BlameState
|
|
||||||
|
|
||||||
data object None : BlameState
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user