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
|
||||
private val _taskEvent = MutableSharedFlow<TaskEvent>()
|
||||
val taskEvent: SharedFlow<TaskEvent> = _taskEvent
|
||||
|
||||
var lastOperation: Long = 0
|
||||
private set
|
||||
|
||||
@ -52,8 +53,11 @@ class TabState @Inject constructor(
|
||||
return unsafeGit
|
||||
}
|
||||
|
||||
private val _refreshData = MutableSharedFlow<RefreshType>()
|
||||
val refreshData: SharedFlow<RefreshType> = _refreshData
|
||||
private val refreshData = MutableSharedFlow<RefreshType>()
|
||||
private val closeableViews = ArrayDeque<Int>()
|
||||
|
||||
private val _closeView = MutableSharedFlow<Int>()
|
||||
val closeViewFlow = _closeView.asSharedFlow()
|
||||
|
||||
/**
|
||||
* Property that indicates if a git operation is running
|
||||
@ -130,7 +134,7 @@ class TabState @Inject constructor(
|
||||
lastOperation = System.currentTimeMillis()
|
||||
|
||||
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)
|
||||
} finally {
|
||||
if (refreshType != RefreshType.NONE && (!hasProcessFailed || refreshEvenIfCrashes))
|
||||
_refreshData.emit(refreshType)
|
||||
refreshData.emit(refreshType)
|
||||
|
||||
operationRunning = false
|
||||
lastOperation = System.currentTimeMillis()
|
||||
@ -236,7 +240,7 @@ class TabState @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun refreshData(refreshType: RefreshType) {
|
||||
_refreshData.emit(refreshType)
|
||||
refreshData.emit(refreshType)
|
||||
}
|
||||
|
||||
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() {
|
||||
currentJob?.cancel()
|
||||
}
|
||||
|
@ -1,34 +1,19 @@
|
||||
package com.jetpackduba.gitnuro.ui
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
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.*
|
||||
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.LoadingRepository
|
||||
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.models.Notification
|
||||
import com.jetpackduba.gitnuro.models.NotificationType
|
||||
import com.jetpackduba.gitnuro.theme.AppTheme
|
||||
import com.jetpackduba.gitnuro.ui.components.Notification
|
||||
import com.jetpackduba.gitnuro.ui.dialogs.CloneDialog
|
||||
import com.jetpackduba.gitnuro.ui.dialogs.GpgPasswordDialog
|
||||
import com.jetpackduba.gitnuro.ui.dialogs.SshPasswordDialog
|
||||
import com.jetpackduba.gitnuro.ui.dialogs.UserPasswordDialog
|
||||
import com.jetpackduba.gitnuro.ui.dialogs.CredentialsDialog
|
||||
import com.jetpackduba.gitnuro.ui.dialogs.errors.ErrorDialog
|
||||
import com.jetpackduba.gitnuro.ui.dialogs.settings.SettingsDialog
|
||||
import com.jetpackduba.gitnuro.viewmodels.RepositorySelectionStatus
|
||||
@ -109,7 +94,7 @@ fun AppTab(
|
||||
|
||||
is RepositorySelectionStatus.Open -> {
|
||||
RepositoryOpenPage(
|
||||
tabViewModel = tabViewModel,
|
||||
repositoryOpenViewModel = tabViewModel.repositoryOpenViewModel,
|
||||
onShowSettingsDialog = { showSettingsDialog = 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.updates.Update
|
||||
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.launch
|
||||
import org.eclipse.jgit.lib.RepositoryState
|
||||
@ -37,16 +37,16 @@ import org.eclipse.jgit.revwalk.RevCommit
|
||||
|
||||
@Composable
|
||||
fun RepositoryOpenPage(
|
||||
tabViewModel: TabViewModel,
|
||||
repositoryOpenViewModel: RepositoryOpenViewModel,
|
||||
onShowSettingsDialog: () -> Unit,
|
||||
onShowCloneDialog: () -> Unit,
|
||||
) {
|
||||
val repositoryState by tabViewModel.repositoryState.collectAsState()
|
||||
val diffSelected by tabViewModel.diffSelected.collectAsState()
|
||||
val selectedItem by tabViewModel.selectedItem.collectAsState()
|
||||
val blameState by tabViewModel.blameState.collectAsState()
|
||||
val showHistory by tabViewModel.showHistory.collectAsState()
|
||||
val showAuthorInfo by tabViewModel.showAuthorInfo.collectAsState()
|
||||
val repositoryState by repositoryOpenViewModel.repositoryState.collectAsState()
|
||||
val diffSelected by repositoryOpenViewModel.diffSelected.collectAsState()
|
||||
val selectedItem by repositoryOpenViewModel.selectedItem.collectAsState()
|
||||
val blameState by repositoryOpenViewModel.blameState.collectAsState()
|
||||
val showHistory by repositoryOpenViewModel.showHistory.collectAsState()
|
||||
val showAuthorInfo by repositoryOpenViewModel.showAuthorInfo.collectAsState()
|
||||
|
||||
var showNewBranchDialog by remember { mutableStateOf(false) }
|
||||
var showStashWithMessageDialog by remember { mutableStateOf(false) }
|
||||
@ -59,7 +59,7 @@ fun RepositoryOpenPage(
|
||||
showNewBranchDialog = false
|
||||
},
|
||||
onAccept = { branchName ->
|
||||
tabViewModel.createBranch(branchName)
|
||||
repositoryOpenViewModel.createBranch(branchName)
|
||||
showNewBranchDialog = false
|
||||
}
|
||||
)
|
||||
@ -69,17 +69,17 @@ fun RepositoryOpenPage(
|
||||
showStashWithMessageDialog = false
|
||||
},
|
||||
onAccept = { stashMessage ->
|
||||
tabViewModel.stashWithMessage(stashMessage)
|
||||
repositoryOpenViewModel.stashWithMessage(stashMessage)
|
||||
showStashWithMessageDialog = false
|
||||
}
|
||||
)
|
||||
} else if (showAuthorInfo) {
|
||||
val authorViewModel = tabViewModel.authorViewModel
|
||||
val authorViewModel = repositoryOpenViewModel.authorViewModel
|
||||
if (authorViewModel != null) {
|
||||
AuthorDialog(
|
||||
authorViewModel = authorViewModel,
|
||||
onClose = {
|
||||
tabViewModel.closeAuthorInfoDialog()
|
||||
repositoryOpenViewModel.closeAuthorInfoDialog()
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -89,16 +89,16 @@ fun RepositoryOpenPage(
|
||||
onAction = {
|
||||
showQuickActionsDialog = false
|
||||
when (it) {
|
||||
QuickActionType.OPEN_DIR_IN_FILE_MANAGER -> tabViewModel.openFolderInFileExplorer()
|
||||
QuickActionType.OPEN_DIR_IN_FILE_MANAGER -> repositoryOpenViewModel.openFolderInFileExplorer()
|
||||
QuickActionType.CLONE -> onShowCloneDialog()
|
||||
QuickActionType.REFRESH -> tabViewModel.refreshAll()
|
||||
QuickActionType.REFRESH -> repositoryOpenViewModel.refreshAll()
|
||||
QuickActionType.SIGN_OFF -> showSignOffDialog = true
|
||||
}
|
||||
},
|
||||
)
|
||||
} else if (showSignOffDialog) {
|
||||
SignOffDialog(
|
||||
viewModel = tabViewModel.tabViewModelsProvider.signOffDialogViewModel,
|
||||
viewModel = repositoryOpenViewModel.tabViewModelsProvider.signOffDialogViewModel,
|
||||
onClose = { showSignOffDialog = false },
|
||||
)
|
||||
}
|
||||
@ -113,11 +113,11 @@ fun RepositoryOpenPage(
|
||||
println("Key event $it")
|
||||
when {
|
||||
it.matchesBinding(KeybindingOption.PULL) -> {
|
||||
tabViewModel.pull(PullType.DEFAULT)
|
||||
repositoryOpenViewModel.pull(PullType.DEFAULT)
|
||||
true
|
||||
}
|
||||
it.matchesBinding(KeybindingOption.PUSH) -> {
|
||||
tabViewModel.push()
|
||||
repositoryOpenViewModel.push()
|
||||
true
|
||||
}
|
||||
it.matchesBinding(KeybindingOption.BRANCH_CREATE) -> {
|
||||
@ -129,11 +129,15 @@ fun RepositoryOpenPage(
|
||||
}
|
||||
}
|
||||
it.matchesBinding(KeybindingOption.STASH) -> {
|
||||
tabViewModel.stash()
|
||||
repositoryOpenViewModel.stash()
|
||||
true
|
||||
}
|
||||
it.matchesBinding(KeybindingOption.STASH_POP) -> {
|
||||
tabViewModel.popStash()
|
||||
repositoryOpenViewModel.popStash()
|
||||
true
|
||||
}
|
||||
it.matchesBinding(KeybindingOption.EXIT) -> {
|
||||
repositoryOpenViewModel.closeLastView()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
@ -148,7 +152,7 @@ fun RepositoryOpenPage(
|
||||
.focusable()
|
||||
.onKeyEvent { keyEvent ->
|
||||
if (keyEvent.matchesBinding(KeybindingOption.REFRESH)) {
|
||||
tabViewModel.refreshAll()
|
||||
repositoryOpenViewModel.refreshAll()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@ -157,7 +161,7 @@ fun RepositoryOpenPage(
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Menu(
|
||||
menuViewModel = tabViewModel.tabViewModelsProvider.menuViewModel,
|
||||
menuViewModel = repositoryOpenViewModel.tabViewModelsProvider.menuViewModel,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
vertical = 4.dp
|
||||
@ -165,12 +169,12 @@ fun RepositoryOpenPage(
|
||||
.fillMaxWidth(),
|
||||
onCreateBranch = { showNewBranchDialog = true },
|
||||
onStashWithMessage = { showStashWithMessageDialog = true },
|
||||
onOpenAnotherRepository = { tabViewModel.openAnotherRepository(it) },
|
||||
onOpenAnotherRepository = { repositoryOpenViewModel.openAnotherRepository(it) },
|
||||
onOpenAnotherRepositoryFromPicker = {
|
||||
val repoToOpen = tabViewModel.openDirectoryPicker()
|
||||
val repoToOpen = repositoryOpenViewModel.openDirectoryPicker()
|
||||
|
||||
if (repoToOpen != null) {
|
||||
tabViewModel.openAnotherRepository(repoToOpen)
|
||||
repositoryOpenViewModel.openAnotherRepository(repoToOpen)
|
||||
}
|
||||
},
|
||||
onQuickActions = { showQuickActionsDialog = true },
|
||||
@ -178,7 +182,7 @@ fun RepositoryOpenPage(
|
||||
)
|
||||
|
||||
RepoContent(
|
||||
tabViewModel = tabViewModel,
|
||||
repositoryOpenViewModel = repositoryOpenViewModel,
|
||||
diffSelected = diffSelected,
|
||||
selectedItem = selectedItem,
|
||||
repositoryState = repositoryState,
|
||||
@ -197,14 +201,14 @@ fun RepositoryOpenPage(
|
||||
)
|
||||
|
||||
|
||||
val userInfo by tabViewModel.authorInfoSimple.collectAsState()
|
||||
val newUpdate = tabViewModel.update.collectAsState().value
|
||||
val userInfo by repositoryOpenViewModel.authorInfoSimple.collectAsState()
|
||||
val newUpdate = repositoryOpenViewModel.update.collectAsState().value
|
||||
|
||||
BottomInfoBar(
|
||||
userInfo,
|
||||
newUpdate,
|
||||
onOpenUrlInBrowser = { tabViewModel.openUrlInBrowser(it) },
|
||||
onShowAuthorInfoDialog = { tabViewModel.showAuthorInfoDialog() },
|
||||
onOpenUrlInBrowser = { repositoryOpenViewModel.openUrlInBrowser(it) },
|
||||
onShowAuthorInfoDialog = { repositoryOpenViewModel.showAuthorInfoDialog() },
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -258,7 +262,7 @@ private fun BottomInfoBar(
|
||||
|
||||
@Composable
|
||||
fun RepoContent(
|
||||
tabViewModel: TabViewModel,
|
||||
repositoryOpenViewModel: RepositoryOpenViewModel,
|
||||
diffSelected: DiffType?,
|
||||
selectedItem: SelectedItem,
|
||||
repositoryState: RepositoryState,
|
||||
@ -266,19 +270,19 @@ fun RepoContent(
|
||||
showHistory: Boolean,
|
||||
) {
|
||||
if (showHistory) {
|
||||
val historyViewModel = tabViewModel.historyViewModel
|
||||
val historyViewModel = repositoryOpenViewModel.historyViewModel
|
||||
|
||||
if (historyViewModel != null) {
|
||||
FileHistory(
|
||||
historyViewModel = historyViewModel,
|
||||
onClose = {
|
||||
tabViewModel.closeHistory()
|
||||
repositoryOpenViewModel.closeHistory()
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
MainContentView(
|
||||
tabViewModel,
|
||||
repositoryOpenViewModel,
|
||||
diffSelected,
|
||||
selectedItem,
|
||||
repositoryState,
|
||||
@ -289,25 +293,25 @@ fun RepoContent(
|
||||
|
||||
@Composable
|
||||
fun MainContentView(
|
||||
tabViewModel: TabViewModel,
|
||||
repositoryOpenViewModel: RepositoryOpenViewModel,
|
||||
diffSelected: DiffType?,
|
||||
selectedItem: SelectedItem,
|
||||
repositoryState: RepositoryState,
|
||||
blameState: BlameState,
|
||||
) {
|
||||
val rebaseInteractiveState by tabViewModel.rebaseInteractiveState.collectAsState()
|
||||
val rebaseInteractiveState by repositoryOpenViewModel.rebaseInteractiveState.collectAsState()
|
||||
val density = LocalDensity.current.density
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// 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 thirdWidth by remember(tabViewModel) { mutableStateOf(tabViewModel.thirdPaneWidth.value) }
|
||||
var firstWidth by remember(repositoryOpenViewModel) { mutableStateOf(repositoryOpenViewModel.firstPaneWidth.value) }
|
||||
var thirdWidth by remember(repositoryOpenViewModel) { mutableStateOf(repositoryOpenViewModel.thirdPaneWidth.value) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// Update the pane widths if they have been changed in a different tab
|
||||
tabViewModel.onPanelsWidthPersisted.collectLatest {
|
||||
firstWidth = tabViewModel.firstPaneWidth.value
|
||||
thirdWidth = tabViewModel.thirdPaneWidth.value
|
||||
repositoryOpenViewModel.onPanelsWidthPersisted.collectLatest {
|
||||
firstWidth = repositoryOpenViewModel.firstPaneWidth.value
|
||||
thirdWidth = repositoryOpenViewModel.thirdPaneWidth.value
|
||||
}
|
||||
}
|
||||
|
||||
@ -317,9 +321,9 @@ fun MainContentView(
|
||||
thirdWidth = thirdWidth,
|
||||
first = {
|
||||
SidePanel(
|
||||
tabViewModel.tabViewModelsProvider.sidePanelViewModel,
|
||||
changeDefaultUpstreamBranchViewModel = { tabViewModel.tabViewModelsProvider.changeDefaultUpstreamBranchViewModel },
|
||||
submoduleDialogViewModel = { tabViewModel.tabViewModelsProvider.submoduleDialogViewModel },
|
||||
repositoryOpenViewModel.tabViewModelsProvider.sidePanelViewModel,
|
||||
changeDefaultUpstreamBranchViewModel = { repositoryOpenViewModel.tabViewModelsProvider.changeDefaultUpstreamBranchViewModel },
|
||||
submoduleDialogViewModel = { repositoryOpenViewModel.tabViewModelsProvider.submoduleDialogViewModel },
|
||||
)
|
||||
},
|
||||
second = {
|
||||
@ -328,13 +332,13 @@ fun MainContentView(
|
||||
.fillMaxSize()
|
||||
) {
|
||||
if (rebaseInteractiveState == RebaseInteractiveState.AwaitingInteraction && diffSelected == null) {
|
||||
RebaseInteractive(tabViewModel.tabViewModelsProvider.rebaseInteractiveViewModel)
|
||||
RebaseInteractive(repositoryOpenViewModel.tabViewModelsProvider.rebaseInteractiveViewModel)
|
||||
} else if (blameState is BlameState.Loaded && !blameState.isMinimized) {
|
||||
Blame(
|
||||
filePath = blameState.filePath,
|
||||
blameResult = blameState.blameResult,
|
||||
onClose = { tabViewModel.resetBlameState() },
|
||||
onSelectCommit = { tabViewModel.selectCommit(it) }
|
||||
onClose = { repositoryOpenViewModel.resetBlameState() },
|
||||
onSelectCommit = { repositoryOpenViewModel.selectCommit(it) }
|
||||
)
|
||||
} else {
|
||||
Column {
|
||||
@ -342,21 +346,21 @@ fun MainContentView(
|
||||
when (diffSelected) {
|
||||
null -> {
|
||||
Log(
|
||||
logViewModel = tabViewModel.tabViewModelsProvider.logViewModel,
|
||||
logViewModel = repositoryOpenViewModel.tabViewModelsProvider.logViewModel,
|
||||
selectedItem = selectedItem,
|
||||
repositoryState = repositoryState,
|
||||
changeDefaultUpstreamBranchViewModel = { tabViewModel.tabViewModelsProvider.changeDefaultUpstreamBranchViewModel },
|
||||
changeDefaultUpstreamBranchViewModel = { repositoryOpenViewModel.tabViewModelsProvider.changeDefaultUpstreamBranchViewModel },
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val diffViewModel = tabViewModel.diffViewModel
|
||||
val diffViewModel = repositoryOpenViewModel.diffViewModel
|
||||
|
||||
if (diffViewModel != null) {
|
||||
Diff(
|
||||
diffViewModel = diffViewModel,
|
||||
onCloseDiffView = {
|
||||
tabViewModel.newDiffSelected = null
|
||||
repositoryOpenViewModel.newDiffSelected = null
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -367,8 +371,8 @@ fun MainContentView(
|
||||
if (blameState is BlameState.Loaded) { // BlameState.isMinimized is true here
|
||||
MinimizedBlame(
|
||||
filePath = blameState.filePath,
|
||||
onExpand = { tabViewModel.expandBlame() },
|
||||
onClose = { tabViewModel.resetBlameState() }
|
||||
onExpand = { repositoryOpenViewModel.expandBlame() },
|
||||
onClose = { repositoryOpenViewModel.resetBlameState() }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -383,13 +387,13 @@ fun MainContentView(
|
||||
when (selectedItem) {
|
||||
SelectedItem.UncommittedChanges -> {
|
||||
UncommittedChanges(
|
||||
statusViewModel = tabViewModel.tabViewModelsProvider.statusViewModel,
|
||||
statusViewModel = repositoryOpenViewModel.tabViewModelsProvider.statusViewModel,
|
||||
selectedEntryType = diffSelected,
|
||||
repositoryState = repositoryState,
|
||||
onStagedDiffEntrySelected = { diffEntry ->
|
||||
tabViewModel.minimizeBlame()
|
||||
repositoryOpenViewModel.minimizeBlame()
|
||||
|
||||
tabViewModel.newDiffSelected = if (diffEntry != null) {
|
||||
repositoryOpenViewModel.newDiffSelected = if (diffEntry != null) {
|
||||
if (repositoryState == RepositoryState.SAFE)
|
||||
DiffType.SafeStagedDiff(diffEntry)
|
||||
else
|
||||
@ -399,29 +403,29 @@ fun MainContentView(
|
||||
}
|
||||
},
|
||||
onUnstagedDiffEntrySelected = { diffEntry ->
|
||||
tabViewModel.minimizeBlame()
|
||||
repositoryOpenViewModel.minimizeBlame()
|
||||
|
||||
if (repositoryState == RepositoryState.SAFE)
|
||||
tabViewModel.newDiffSelected = DiffType.SafeUnstagedDiff(diffEntry)
|
||||
repositoryOpenViewModel.newDiffSelected = DiffType.SafeUnstagedDiff(diffEntry)
|
||||
else
|
||||
tabViewModel.newDiffSelected = DiffType.UnsafeUnstagedDiff(diffEntry)
|
||||
repositoryOpenViewModel.newDiffSelected = DiffType.UnsafeUnstagedDiff(diffEntry)
|
||||
},
|
||||
onBlameFile = { tabViewModel.blameFile(it) },
|
||||
onHistoryFile = { tabViewModel.fileHistory(it) }
|
||||
onBlameFile = { repositoryOpenViewModel.blameFile(it) },
|
||||
onHistoryFile = { repositoryOpenViewModel.fileHistory(it) }
|
||||
)
|
||||
}
|
||||
|
||||
is SelectedItem.CommitBasedItem -> {
|
||||
CommitChanges(
|
||||
commitChangesViewModel = tabViewModel.tabViewModelsProvider.commitChangesViewModel,
|
||||
commitChangesViewModel = repositoryOpenViewModel.tabViewModelsProvider.commitChangesViewModel,
|
||||
selectedItem = selectedItem,
|
||||
diffSelected = diffSelected,
|
||||
onDiffSelected = { diffEntry ->
|
||||
tabViewModel.minimizeBlame()
|
||||
tabViewModel.newDiffSelected = DiffType.CommitDiff(diffEntry)
|
||||
repositoryOpenViewModel.minimizeBlame()
|
||||
repositoryOpenViewModel.newDiffSelected = DiffType.CommitDiff(diffEntry)
|
||||
},
|
||||
onBlame = { tabViewModel.blameFile(it) },
|
||||
onHistory = { tabViewModel.fileHistory(it) },
|
||||
onBlame = { repositoryOpenViewModel.blameFile(it) },
|
||||
onHistory = { repositoryOpenViewModel.fileHistory(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@ -431,19 +435,19 @@ fun MainContentView(
|
||||
},
|
||||
onFirstSizeDragStarted = { currentWidth ->
|
||||
firstWidth = currentWidth
|
||||
tabViewModel.setFirstPaneWidth(currentWidth)
|
||||
repositoryOpenViewModel.setFirstPaneWidth(currentWidth)
|
||||
},
|
||||
onFirstSizeChange = {
|
||||
val newWidth = firstWidth + it / density
|
||||
|
||||
if (newWidth > 150 && rebaseInteractiveState !is RebaseInteractiveState.AwaitingInteraction) {
|
||||
firstWidth = newWidth
|
||||
tabViewModel.setFirstPaneWidth(newWidth)
|
||||
repositoryOpenViewModel.setFirstPaneWidth(newWidth)
|
||||
}
|
||||
},
|
||||
onFirstSizeDragStopped = {
|
||||
scope.launch {
|
||||
tabViewModel.persistFirstPaneWidth()
|
||||
repositoryOpenViewModel.persistFirstPaneWidth()
|
||||
}
|
||||
},
|
||||
onThirdSizeChange = {
|
||||
@ -451,16 +455,16 @@ fun MainContentView(
|
||||
|
||||
if (newWidth > 150) {
|
||||
thirdWidth = newWidth
|
||||
tabViewModel.setThirdPaneWidth(newWidth)
|
||||
repositoryOpenViewModel.setThirdPaneWidth(newWidth)
|
||||
}
|
||||
},
|
||||
onThirdSizeDragStarted = { currentWidth ->
|
||||
thirdWidth = currentWidth
|
||||
tabViewModel.setThirdPaneWidth(currentWidth)
|
||||
repositoryOpenViewModel.setThirdPaneWidth(currentWidth)
|
||||
},
|
||||
onThirdSizeDragStopped = {
|
||||
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(
|
||||
private val updatesWebService: UpdatesService,
|
||||
) {
|
||||
fun hasUpdatesFlow() = flow {
|
||||
val hasUpdatesFlow = flow {
|
||||
val latestReleaseJson = updatesWebService.release(AppConstants.VERSION_CHECK_URL)
|
||||
|
||||
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 openRepositoryUseCase: OpenRepositoryUseCase,
|
||||
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,
|
||||
val appStateManager: AppStateManager,
|
||||
private val fileChangesWatcher: FileChangesWatcher,
|
||||
private val updatesRepository: UpdatesRepository,
|
||||
private val credentialsStateManager: CredentialsStateManager,
|
||||
private val createBranchUseCase: CreateBranchUseCase,
|
||||
private val stashChangesUseCase: StashChangesUseCase,
|
||||
private val stageUntrackedFileUseCase: StageUntrackedFileUseCase,
|
||||
private val openFilePickerUseCase: OpenFilePickerUseCase,
|
||||
private val openUrlInBrowserUseCase: OpenUrlInBrowserUseCase,
|
||||
private val sharedRepositoryStateManager: SharedRepositoryStateManager,
|
||||
private val tabsManager: TabsManager,
|
||||
private val tabScope: CoroutineScope,
|
||||
private val verticalSplitPaneConfig: VerticalSplitPaneConfig,
|
||||
val tabViewModelsProvider: TabViewModelsProvider,
|
||||
private val globalMenuActionsViewModel: GlobalMenuActionsViewModel,
|
||||
private val repositoryOpenViewModelProvider: Provider<RepositoryOpenViewModel>,
|
||||
updatesRepository: UpdatesRepository,
|
||||
) : IVerticalSplitPaneConfig by verticalSplitPaneConfig,
|
||||
IGlobalMenuActionsViewModel by globalMenuActionsViewModel {
|
||||
var initialPath: String? = null // Stores the path that should be opened when the tab is selected
|
||||
val errorsManager: ErrorsManager = tabState.errorsManager
|
||||
val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem
|
||||
var diffViewModel: DiffViewModel? = null
|
||||
|
||||
val repositoryOpenViewModel: RepositoryOpenViewModel by lazy {
|
||||
repositoryOpenViewModelProvider.get()
|
||||
}
|
||||
private val _repositorySelectionStatus = MutableStateFlow<RepositorySelectionStatus>(RepositorySelectionStatus.None)
|
||||
val repositorySelectionStatus: StateFlow<RepositorySelectionStatus>
|
||||
get() = _repositorySelectionStatus
|
||||
|
||||
val repositoryState: StateFlow<RepositoryState> = sharedRepositoryStateManager.repositoryState
|
||||
val rebaseInteractiveState: StateFlow<RebaseInteractiveState> = sharedRepositoryStateManager.rebaseInteractiveState
|
||||
|
||||
val processing: StateFlow<ProcessingState> = tabState.processing
|
||||
|
||||
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)
|
||||
|
||||
init {
|
||||
tabScope.run {
|
||||
launch {
|
||||
tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REPO_STATE) {
|
||||
loadAuthorInfo(tabState.git)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
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) {
|
||||
openRepository(File(directory))
|
||||
}
|
||||
@ -182,10 +127,6 @@ class TabViewModel @Inject constructor(
|
||||
|
||||
onRepositoryChanged(path)
|
||||
tabState.newSelectedItem(selectedItem = SelectedItem.UncommittedChanges)
|
||||
newDiffSelected = null
|
||||
refreshRepositoryInfo()
|
||||
|
||||
watchRepositoryChanges(git)
|
||||
} catch (ex: Exception) {
|
||||
onRepositoryChanged(null)
|
||||
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() {
|
||||
credentialsStateManager.updateState(CredentialsState.CredentialsDenied)
|
||||
}
|
||||
@ -322,22 +160,6 @@ class TabViewModel @Inject constructor(
|
||||
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? {
|
||||
val latestDirectoryOpened = appStateManager.latestOpenedRepositoryPath
|
||||
|
||||
@ -350,105 +172,9 @@ class TabViewModel @Inject constructor(
|
||||
openRepository(repoDir)
|
||||
}
|
||||
|
||||
val update: StateFlow<Update?> = updatesRepository.hasUpdatesFlow()
|
||||
.flowOn(Dispatchers.IO)
|
||||
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 gpgCredentialsAccepted(password: String) {
|
||||
credentialsStateManager.updateState(CredentialsAccepted.GpgCredentialsAccepted(password))
|
||||
}
|
||||
@ -468,15 +194,7 @@ class TabViewModel @Inject constructor(
|
||||
|
||||
|
||||
sealed class RepositorySelectionStatus {
|
||||
object None : RepositorySelectionStatus()
|
||||
data object None : RepositorySelectionStatus()
|
||||
data class Opening(val path: String) : 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