From 9d07ac59b7b80fc1ed1782e94635da3d7d1b88b6 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Sun, 29 Jan 2023 19:27:17 +0100 Subject: [PATCH] Implemented search in side panel Refactored composables to use a lazy column instead of a normal column, as it had performance issues with large repositories. Fixes #43 --- .../factories/SidePanelViewModelsFactory.kt | 28 ++ .../gitnuro/extensions/StringExtensions.kt | 6 +- .../gitnuro/theme/ComponentsColors.kt | 1 + .../com/jetpackduba/gitnuro/ui/Branches.kt | 104 ----- .../com/jetpackduba/gitnuro/ui/Remotes.kt | 123 ----- .../jetpackduba/gitnuro/ui/RepositoryOpen.kt | 50 +-- .../com/jetpackduba/gitnuro/ui/SidePanel.kt | 419 ++++++++++++++++++ .../com/jetpackduba/gitnuro/ui/Stashes.kt | 70 --- .../com/jetpackduba/gitnuro/ui/Submodules.kt | 2 +- .../kotlin/com/jetpackduba/gitnuro/ui/Tags.kt | 65 --- .../components/AdjustableOutlinedTextField.kt | 41 +- .../gitnuro/ui/components/SideMenuEntry.kt | 7 +- .../gitnuro/ui/components/SideMenuPanel.kt | 2 +- .../gitnuro/ui/dialogs/EditRemotesDialog.kt | 15 +- .../gitnuro/viewmodels/TabViewModelsHolder.kt | 13 +- .../{ => sidepanel}/BranchesViewModel.kt | 42 +- .../{ => sidepanel}/RemotesViewModel.kt | 71 ++- .../SidePanelChildViewModel.kt} | 4 +- .../sidepanel/SidePanelViewModel.kt | 28 ++ .../{ => sidepanel}/StashesViewModel.kt | 39 +- .../{ => sidepanel}/SubmodulesViewModel.kt | 4 +- .../{ => sidepanel}/TagsViewModel.kt | 37 +- src/main/resources/search.svg | 1 + 23 files changed, 666 insertions(+), 506 deletions(-) create mode 100644 src/main/kotlin/com/jetpackduba/gitnuro/di/factories/SidePanelViewModelsFactory.kt delete mode 100644 src/main/kotlin/com/jetpackduba/gitnuro/ui/Branches.kt delete mode 100644 src/main/kotlin/com/jetpackduba/gitnuro/ui/Remotes.kt create mode 100644 src/main/kotlin/com/jetpackduba/gitnuro/ui/SidePanel.kt delete mode 100644 src/main/kotlin/com/jetpackduba/gitnuro/ui/Stashes.kt delete mode 100644 src/main/kotlin/com/jetpackduba/gitnuro/ui/Tags.kt rename src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/{ => sidepanel}/BranchesViewModel.kt (77%) rename src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/{ => sidepanel}/RemotesViewModel.kt (69%) rename src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/{ExpandableViewModel.kt => sidepanel/SidePanelChildViewModel.kt} (70%) create mode 100644 src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/SidePanelViewModel.kt rename src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/{ => sidepanel}/StashesViewModel.kt (70%) rename src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/{ => sidepanel}/SubmodulesViewModel.kt (95%) rename src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/{ => sidepanel}/TagsViewModel.kt (61%) create mode 100644 src/main/resources/search.svg diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/di/factories/SidePanelViewModelsFactory.kt b/src/main/kotlin/com/jetpackduba/gitnuro/di/factories/SidePanelViewModelsFactory.kt new file mode 100644 index 0000000..68be4c9 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/di/factories/SidePanelViewModelsFactory.kt @@ -0,0 +1,28 @@ +package com.jetpackduba.gitnuro.di.factories + +import com.jetpackduba.gitnuro.viewmodels.sidepanel.BranchesViewModel +import com.jetpackduba.gitnuro.viewmodels.sidepanel.RemotesViewModel +import com.jetpackduba.gitnuro.viewmodels.sidepanel.StashesViewModel +import com.jetpackduba.gitnuro.viewmodels.sidepanel.TagsViewModel +import dagger.assisted.AssistedFactory +import kotlinx.coroutines.flow.StateFlow + +@AssistedFactory +interface BranchesViewModelFactory { + fun create(filter: StateFlow): BranchesViewModel +} + +@AssistedFactory +interface RemotesViewModelFactory { + fun create(filter: StateFlow): RemotesViewModel +} + +@AssistedFactory +interface TagsViewModelFactory { + fun create(filter: StateFlow): TagsViewModel +} + +@AssistedFactory +interface StashesViewModelFactory { + fun create(filter: StateFlow): StashesViewModel +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/extensions/StringExtensions.kt b/src/main/kotlin/com/jetpackduba/gitnuro/extensions/StringExtensions.kt index 26bb5f4..dcb3d5f 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/extensions/StringExtensions.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/extensions/StringExtensions.kt @@ -46,4 +46,8 @@ val String.lineDelimiter: String? } val String.nullIfEmpty: String? - get() = this.ifBlank { null } \ No newline at end of file + get() = this.ifBlank { null } + +fun String.lowercaseContains(other: String) : Boolean { + return this.lowercase().contains(other.lowercase().trim()) +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/theme/ComponentsColors.kt b/src/main/kotlin/com/jetpackduba/gitnuro/theme/ComponentsColors.kt index 2eaddf4..9f9d1d3 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/theme/ComponentsColors.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/theme/ComponentsColors.kt @@ -27,6 +27,7 @@ fun textFieldColors( fun outlinedTextFieldColors() = TextFieldDefaults.outlinedTextFieldColors( cursorColor = MaterialTheme.colors.primaryVariant, focusedBorderColor = MaterialTheme.colors.primaryVariant, + unfocusedBorderColor = MaterialTheme.colors.onBackgroundSecondary.copy(alpha = 0.2f), focusedLabelColor = MaterialTheme.colors.primaryVariant, backgroundColor = MaterialTheme.colors.background, textColor = MaterialTheme.colors.onBackground, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Branches.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Branches.kt deleted file mode 100644 index c49b9a2..0000000 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Branches.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.jetpackduba.gitnuro.ui - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.padding -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import com.jetpackduba.gitnuro.extensions.isLocal -import com.jetpackduba.gitnuro.extensions.simpleName -import com.jetpackduba.gitnuro.theme.onBackgroundSecondary -import com.jetpackduba.gitnuro.ui.components.SideMenuPanel -import com.jetpackduba.gitnuro.ui.components.SideMenuSubentry -import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel -import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu -import com.jetpackduba.gitnuro.ui.context_menu.branchContextMenuItems -import com.jetpackduba.gitnuro.viewmodels.BranchesViewModel -import org.eclipse.jgit.lib.Ref - -@Composable -fun Branches( - branchesViewModel: BranchesViewModel = gitnuroViewModel(), -) { - val branches by branchesViewModel.branches.collectAsState() - val currentBranchState = branchesViewModel.currentBranch.collectAsState() - val isExpanded by branchesViewModel.isExpanded.collectAsState() - val currentBranch = currentBranchState.value - - SideMenuPanel( - title = "Local branches", - icon = painterResource("branch.svg"), - items = branches, - isExpanded = isExpanded, - onExpand = { branchesViewModel.onExpand() }, - itemContent = { branch -> - BranchLineEntry( - branch = branch, - currentBranch = currentBranch, - isCurrentBranch = currentBranch?.name == branch.name, - onBranchClicked = { branchesViewModel.selectBranch(branch) }, - onBranchDoubleClicked = { branchesViewModel.checkoutRef(branch) }, - onCheckoutBranch = { branchesViewModel.checkoutRef(branch) }, - onMergeBranch = { branchesViewModel.mergeBranch(branch) }, - onDeleteBranch = { branchesViewModel.deleteBranch(branch) }, - onRebaseBranch = { branchesViewModel.rebaseBranch(branch) }, - onPushToRemoteBranch = { branchesViewModel.pushToRemoteBranch(branch) }, - onPullFromRemoteBranch = { branchesViewModel.pullFromRemoteBranch(branch) }, - ) - } - ) -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun BranchLineEntry( - branch: Ref, - currentBranch: Ref?, - isCurrentBranch: Boolean, - onBranchClicked: () -> Unit, - onBranchDoubleClicked: () -> Unit, - onCheckoutBranch: () -> Unit, - onMergeBranch: () -> Unit, - onRebaseBranch: () -> Unit, - onDeleteBranch: () -> Unit, - onPushToRemoteBranch: () -> Unit, - onPullFromRemoteBranch: () -> Unit, -) { - ContextMenu( - items = { - branchContextMenuItems( - branch = branch, - currentBranch = currentBranch, - isCurrentBranch = isCurrentBranch, - isLocal = branch.isLocal, - onCheckoutBranch = onCheckoutBranch, - onMergeBranch = onMergeBranch, - onDeleteBranch = onDeleteBranch, - onRebaseBranch = onRebaseBranch, - onPushToRemoteBranch = onPushToRemoteBranch, - onPullFromRemoteBranch = onPullFromRemoteBranch, - ) - } - ) { - SideMenuSubentry( - text = branch.simpleName, - iconResourcePath = "branch.svg", - onClick = onBranchClicked, - onDoubleClick = onBranchDoubleClicked, - ) { - if (isCurrentBranch) { - Text( - text = "HEAD", - color = MaterialTheme.colors.onBackgroundSecondary, - style = MaterialTheme.typography.caption, - modifier = Modifier.padding(horizontal = 16.dp), - ) - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Remotes.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Remotes.kt deleted file mode 100644 index 42f22b0..0000000 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Remotes.kt +++ /dev/null @@ -1,123 +0,0 @@ -@file:OptIn(ExperimentalComposeUiApi::class) - -package com.jetpackduba.gitnuro.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.* -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import com.jetpackduba.gitnuro.extensions.handOnHover -import com.jetpackduba.gitnuro.extensions.simpleName -import com.jetpackduba.gitnuro.ui.components.SideMenuPanel -import com.jetpackduba.gitnuro.ui.components.SideMenuSubentry -import com.jetpackduba.gitnuro.ui.components.VerticalExpandable -import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel -import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu -import com.jetpackduba.gitnuro.ui.context_menu.remoteBranchesContextMenu -import com.jetpackduba.gitnuro.ui.context_menu.remoteContextMenu -import com.jetpackduba.gitnuro.ui.dialogs.EditRemotesDialog -import com.jetpackduba.gitnuro.viewmodels.RemoteView -import com.jetpackduba.gitnuro.viewmodels.RemotesViewModel -import org.eclipse.jgit.lib.Ref - -@Composable -fun Remotes( - remotesViewModel: RemotesViewModel = gitnuroViewModel(), -) { - val remotes by remotesViewModel.remotes.collectAsState() - var showEditRemotesDialog by remember { mutableStateOf(false) } - val isExpanded by remotesViewModel.isExpanded.collectAsState() - - if (showEditRemotesDialog) { - EditRemotesDialog( - remotesViewModel = remotesViewModel, - onDismiss = { - showEditRemotesDialog = false - }, - ) - } - - SideMenuPanel( - title = "Remotes", - icon = painterResource("cloud.svg"), - items = remotes, - isExpanded = isExpanded, - onExpand = { remotesViewModel.onExpand() }, - contextItems = { - remoteContextMenu { showEditRemotesDialog = true } - }, - headerHoverIcon = { - IconButton( - onClick = { showEditRemotesDialog = true }, - modifier = Modifier - .padding(end = 16.dp) - .size(16.dp) - .handOnHover(), - ) { - Icon( - painter = painterResource("settings.svg"), - contentDescription = null, - modifier = Modifier - .fillMaxSize(), - tint = MaterialTheme.colors.onBackground, - ) - } - }, - itemContent = { remoteInfo -> - RemoteRow( - remote = remoteInfo, - onBranchClicked = { branch -> remotesViewModel.selectBranch(branch) }, - onDeleteBranch = { branch -> remotesViewModel.deleteRemoteBranch(branch) }, - onRemoteClicked = { remotesViewModel.onRemoteClicked(remoteInfo) } - ) - }, - ) -} - - -@Composable -private fun RemoteRow( - remote: RemoteView, - onRemoteClicked: () -> Unit, - onBranchClicked: (Ref) -> Unit, - onDeleteBranch: (Ref) -> Unit, -) { - VerticalExpandable( - isExpanded = remote.isExpanded, - onExpand = onRemoteClicked, - header = { - SideMenuSubentry( - text = remote.remoteInfo.remoteConfig.name, - iconResourcePath = "cloud.svg", - ) - } - ) { - val branches = remote.remoteInfo.branchesList - Column { - branches.forEach { branch -> - ContextMenu( - items = { - remoteBranchesContextMenu( - onDeleteBranch = { onDeleteBranch(branch) } - ) - } - ) { - SideMenuSubentry( - text = branch.simpleName, - extraPadding = 24.dp, - iconResourcePath = "branch.svg", - onClick = { onBranchClicked(branch) } - ) - } - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt index eaae051..fc737b3 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt @@ -5,7 +5,6 @@ package com.jetpackduba.gitnuro.ui import androidx.compose.foundation.background import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* @@ -17,17 +16,14 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import com.jetpackduba.gitnuro.extensions.handMouseClickable import com.jetpackduba.gitnuro.git.DiffEntryType import com.jetpackduba.gitnuro.keybindings.KeybindingOption import com.jetpackduba.gitnuro.keybindings.matchesBinding import com.jetpackduba.gitnuro.ui.components.PrimaryButton -import com.jetpackduba.gitnuro.ui.components.ScrollableColumn import com.jetpackduba.gitnuro.ui.dialogs.* import com.jetpackduba.gitnuro.ui.diff.Diff import com.jetpackduba.gitnuro.ui.log.Log @@ -252,39 +248,6 @@ fun RepoContent( } } -@Composable -fun SidePanelOption(title: String, icon: String, onClick: () -> Unit) { - Row( - modifier = Modifier - .height(36.dp) - .fillMaxWidth() - .handMouseClickable(onClick) - .padding(start = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - - Icon( - painter = painterResource(icon), - contentDescription = null, - tint = MaterialTheme.colors.onBackground, - modifier = Modifier - .size(16.dp), - ) - - Text( - text = title, - modifier = Modifier - .padding(horizontal = 8.dp) - .weight(1f), - maxLines = 1, - style = MaterialTheme.typography.body2, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colors.onBackground, - overflow = TextOverflow.Ellipsis, - ) - } -} - @OptIn(ExperimentalSplitPaneApi::class) @Composable fun MainContentView( @@ -298,18 +261,7 @@ fun MainContentView( splitPaneState = rememberSplitPaneState(initialPositionPercentage = 0.20f) ) { first(minSize = 180.dp) { - Column { - ScrollableColumn( - modifier = Modifier - .weight(1f), - ) { - Branches() - Remotes() - Tags() - Stashes() -// TODO: Enable on 1.2.0 when fully implemented Submodules() - } - } + SidePanel() } splitter { diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/SidePanel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/SidePanel.kt new file mode 100644 index 0000000..a61d002 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/SidePanel.kt @@ -0,0 +1,419 @@ +package com.jetpackduba.gitnuro.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.jetpackduba.gitnuro.extensions.handOnHover +import com.jetpackduba.gitnuro.extensions.isLocal +import com.jetpackduba.gitnuro.extensions.simpleName +import com.jetpackduba.gitnuro.theme.onBackgroundSecondary +import com.jetpackduba.gitnuro.ui.components.* +import com.jetpackduba.gitnuro.ui.context_menu.* +import com.jetpackduba.gitnuro.ui.dialogs.EditRemotesDialog +import com.jetpackduba.gitnuro.viewmodels.sidepanel.* +import org.eclipse.jgit.lib.Ref +import org.eclipse.jgit.revwalk.RevCommit + +@Composable +fun SidePanel( + sidePanelViewModel: SidePanelViewModel = gitnuroViewModel(), + branchesViewModel: BranchesViewModel = sidePanelViewModel.branchesViewModel, + remotesViewModel: RemotesViewModel = sidePanelViewModel.remotesViewModel, + tagsViewModel: TagsViewModel = sidePanelViewModel.tagsViewModel, + stashesViewModel: StashesViewModel = sidePanelViewModel.stashesViewModel, +) { + var filter by remember(sidePanelViewModel) { mutableStateOf(sidePanelViewModel.filter.value) } + + val branchesState by branchesViewModel.branchesState.collectAsState() + val remotesState by remotesViewModel.remoteState.collectAsState() + val tagsState by tagsViewModel.tagsState.collectAsState() + val stashesState by stashesViewModel.stashesState.collectAsState() + + var showEditRemotesDialog by remember { mutableStateOf(false) } + + Column { + FilterTextField( + value = filter, + onValueChange = { newValue -> + filter = newValue + sidePanelViewModel.newFilter(newValue) + }, + modifier = Modifier + .padding(start = 8.dp) + ) + + ScrollableLazyColumn(modifier = Modifier + .fillMaxSize() + .padding(top = 4.dp) + ) { + localBranches( + branchesState = branchesState, + branchesViewModel = branchesViewModel, + ) + + remotes( + remotesState = remotesState, + remotesViewModel = remotesViewModel, + onShowEditRemotesDialog = { showEditRemotesDialog = true }, + ) + + tags( + tagsState = tagsState, + tagsViewModel = tagsViewModel, + ) + + stashes( + stashesState = stashesState, + stashesViewModel = stashesViewModel, + ) + } + } + + if (showEditRemotesDialog) { + EditRemotesDialog( + remotesViewModel = remotesViewModel, + onDismiss = { + showEditRemotesDialog = false + }, + ) + } +} + +@Composable +fun FilterTextField(value: String, onValueChange: (String) -> Unit, modifier: Modifier) { + AdjustableOutlinedTextField( + value = value, + hint = "Search for branches, tags & more", + onValueChange = onValueChange, + modifier = modifier, + textStyle = LocalTextStyle.current.copy( + fontSize = MaterialTheme.typography.body2.fontSize, + color = MaterialTheme.colors.onBackground, + ), + maxLines = 1, + leadingIcon = { + Icon( + painterResource("search.svg"), + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = if (value.isEmpty()) MaterialTheme.colors.onBackgroundSecondary else MaterialTheme.colors.onBackground + ) + } + ) +} + +fun LazyListScope.localBranches( + branchesState: BranchesState, + branchesViewModel: BranchesViewModel, +) { + val isExpanded = branchesState.isExpanded + val branches = branchesState.branches + val currentBranch = branchesState.currentBranch + + item { + ContextMenu( + items = { emptyList() } + ) { + SideMenuHeader( + text = "Local branches", + icon = painterResource("branch.svg"), + itemsCount = branches.count(), + hoverIcon = null, + isExpanded = isExpanded, + onExpand = { branchesViewModel.onExpand() } + ) + } + } + + if (isExpanded) { + items(branches, key = { it.name }) { branch -> + Branch( + branch = branch, + currentBranch = currentBranch, + isCurrentBranch = currentBranch?.name == branch.name, + onBranchClicked = { branchesViewModel.selectBranch(branch) }, + onBranchDoubleClicked = { branchesViewModel.checkoutRef(branch) }, + onCheckoutBranch = { branchesViewModel.checkoutRef(branch) }, + onMergeBranch = { branchesViewModel.mergeBranch(branch) }, + onDeleteBranch = { branchesViewModel.deleteBranch(branch) }, + onRebaseBranch = { branchesViewModel.rebaseBranch(branch) }, + onPushToRemoteBranch = { branchesViewModel.pushToRemoteBranch(branch) }, + onPullFromRemoteBranch = { branchesViewModel.pullFromRemoteBranch(branch) }, + ) + } + } +} + +fun LazyListScope.remotes( + remotesState: RemotesState, + remotesViewModel: RemotesViewModel, + onShowEditRemotesDialog: () -> Unit, +) { + val isExpanded = remotesState.isExpanded + val remotes = remotesState.remotes + + item { + ContextMenu( + items = { remoteBranchesContextMenu(onShowEditRemotesDialog) } + ) { + SideMenuHeader( + text = "Remotes", + icon = painterResource("cloud.svg"), + itemsCount = remotes.count(), + hoverIcon = { + IconButton( + onClick = onShowEditRemotesDialog, + modifier = Modifier + .padding(end = 16.dp) + .size(16.dp) + .handOnHover(), + ) { + Icon( + painter = painterResource("settings.svg"), + contentDescription = null, + modifier = Modifier + .fillMaxSize(), + tint = MaterialTheme.colors.onBackground, + ) + } + }, + isExpanded = isExpanded, + onExpand = { remotesViewModel.onExpand() } + ) + } + } + + if (isExpanded) { + for (remote in remotes) { + item { + Remote( + remote = remote, + onRemoteClicked = { remotesViewModel.onRemoteClicked(remote) } + ) + } + + if (remote.isExpanded) { + items(remote.remoteInfo.branchesList) { remoteBranch -> + RemoteBranches( + remoteBranch = remoteBranch, + onBranchClicked = { remotesViewModel.selectBranch(remoteBranch) }, + onDeleteBranch = { remotesViewModel.deleteRemoteBranch(remoteBranch) }, + ) + } + } + } + } +} + + +fun LazyListScope.tags( + tagsState: TagsState, + tagsViewModel: TagsViewModel, +) { + val isExpanded = tagsState.isExpanded + val tags = tagsState.tags + + item { + ContextMenu( + items = { emptyList() } + ) { + SideMenuHeader( + text = "Tags", + icon = painterResource("tag.svg"), + itemsCount = tags.count(), + hoverIcon = null, + isExpanded = isExpanded, + onExpand = { tagsViewModel.onExpand() } + ) + } + } + + if (isExpanded) { + items(tags, key = { it.name }) { tag -> +// if () { + Tag( + tag, + onTagClicked = { tagsViewModel.selectTag(tag) }, + onCheckoutTag = { tagsViewModel.checkoutRef(tag) }, + onDeleteTag = { tagsViewModel.deleteTag(tag) } + ) +// } + } + } +} + +fun LazyListScope.stashes( + stashesState: StashesState, + stashesViewModel: StashesViewModel, +) { + val isExpanded = stashesState.isExpanded + val stashes = stashesState.stashes + + item { + ContextMenu( + items = { emptyList() } + ) { + SideMenuHeader( + text = "Stashes", + icon = painterResource("stash.svg"), + itemsCount = stashes.count(), + hoverIcon = null, + isExpanded = isExpanded, + onExpand = { stashesViewModel.onExpand() } + ) + } + } + + if (isExpanded) { + items(stashes, key = { it.name }) { stash -> + Stash( + stash, + onClick = { stashesViewModel.selectStash(stash) }, + onApply = { stashesViewModel.applyStash(stash) }, + onPop = { stashesViewModel.popStash(stash) }, + onDelete = { stashesViewModel.deleteStash(stash) }, + ) + } + } +} + +@Composable +private fun Branch( + branch: Ref, + currentBranch: Ref?, + isCurrentBranch: Boolean, + onBranchClicked: () -> Unit, + onBranchDoubleClicked: () -> Unit, + onCheckoutBranch: () -> Unit, + onMergeBranch: () -> Unit, + onRebaseBranch: () -> Unit, + onDeleteBranch: () -> Unit, + onPushToRemoteBranch: () -> Unit, + onPullFromRemoteBranch: () -> Unit, +) { + ContextMenu( + items = { + branchContextMenuItems( + branch = branch, + currentBranch = currentBranch, + isCurrentBranch = isCurrentBranch, + isLocal = branch.isLocal, + onCheckoutBranch = onCheckoutBranch, + onMergeBranch = onMergeBranch, + onDeleteBranch = onDeleteBranch, + onRebaseBranch = onRebaseBranch, + onPushToRemoteBranch = onPushToRemoteBranch, + onPullFromRemoteBranch = onPullFromRemoteBranch, + ) + } + ) { + SideMenuSubentry( + text = branch.simpleName, + iconResourcePath = "branch.svg", + onClick = onBranchClicked, + onDoubleClick = onBranchDoubleClicked, + ) { + if (isCurrentBranch) { + Text( + text = "HEAD", + color = MaterialTheme.colors.onBackgroundSecondary, + style = MaterialTheme.typography.caption, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } + } +} + + +@Composable +private fun Remote( + remote: RemoteView, + onRemoteClicked: () -> Unit, +) { + SideMenuSubentry( + text = remote.remoteInfo.remoteConfig.name, + iconResourcePath = "cloud.svg", + onClick = onRemoteClicked + ) +} + + +@Composable +private fun RemoteBranches( + remoteBranch: Ref, + onBranchClicked: () -> Unit, + onDeleteBranch: () -> Unit, +) { + ContextMenu( + items = { + remoteBranchesContextMenu( + onDeleteBranch = onDeleteBranch + ) + } + ) { + SideMenuSubentry( + text = remoteBranch.simpleName, + extraPadding = 24.dp, + iconResourcePath = "branch.svg", + onClick = onBranchClicked + ) + } +} + +@Composable +private fun Tag( + tag: Ref, + onTagClicked: () -> Unit, + onCheckoutTag: () -> Unit, + onDeleteTag: () -> Unit, +) { + ContextMenu( + items = { + tagContextMenuItems( + onCheckoutTag = onCheckoutTag, + onDeleteTag = onDeleteTag, + ) + } + ) { + SideMenuSubentry( + text = tag.simpleName, + iconResourcePath = "tag.svg", + onClick = onTagClicked, + ) + } +} + + +@Composable +private fun Stash( + stash: RevCommit, + onClick: () -> Unit, + onApply: () -> Unit, + onPop: () -> Unit, + onDelete: () -> Unit, +) { + ContextMenu( + items = { + stashesContextMenuItems( + onApply = onApply, + onPop = onPop, + onDelete = onDelete, + ) + } + ) { + SideMenuSubentry( + text = stash.shortMessage, + iconResourcePath = "stash.svg", + onClick = onClick, + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Stashes.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Stashes.kt deleted file mode 100644 index 3a95053..0000000 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Stashes.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.jetpackduba.gitnuro.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.res.painterResource -import com.jetpackduba.gitnuro.ui.components.SideMenuPanel -import com.jetpackduba.gitnuro.ui.components.SideMenuSubentry -import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel -import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu -import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement -import com.jetpackduba.gitnuro.ui.context_menu.stashesContextMenuItems -import com.jetpackduba.gitnuro.viewmodels.StashStatus -import com.jetpackduba.gitnuro.viewmodels.StashesViewModel -import org.eclipse.jgit.revwalk.RevCommit - -@Composable -fun Stashes( - stashesViewModel: StashesViewModel = gitnuroViewModel(), -) { - val stashStatusState = stashesViewModel.stashStatus.collectAsState() - val stashStatus = stashStatusState.value - val isExpanded by stashesViewModel.isExpanded.collectAsState() - - val stashList = if (stashStatus is StashStatus.Loaded) - stashStatus.stashes - else - listOf() - - SideMenuPanel( - title = "Stashes", - icon = painterResource("stash.svg"), - items = stashList, - isExpanded = isExpanded, - onExpand = { stashesViewModel.onExpand() }, - itemContent = { stash -> - StashRow( - stash = stash, - onClick = { stashesViewModel.selectTab(stash) }, - contextItems = stashesContextMenuItems( - onApply = { stashesViewModel.applyStash(stash) }, - onPop = { - stashesViewModel.popStash(stash) - }, - onDelete = { - stashesViewModel.deleteStash(stash) - }, - ) - ) - } - ) - -} - -@Composable -private fun StashRow( - stash: RevCommit, - onClick: () -> Unit, - contextItems: List, -) { - ContextMenu( - items = { contextItems } - ) { - SideMenuSubentry( - text = stash.shortMessage, - iconResourcePath = "stash.svg", - onClick = onClick, - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Submodules.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Submodules.kt index 39cafbc..8043874 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Submodules.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Submodules.kt @@ -16,7 +16,7 @@ import com.jetpackduba.gitnuro.ui.components.Tooltip import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu import com.jetpackduba.gitnuro.ui.context_menu.submoduleContextMenuItems -import com.jetpackduba.gitnuro.viewmodels.SubmodulesViewModel +import com.jetpackduba.gitnuro.viewmodels.sidepanel.SubmodulesViewModel import org.eclipse.jgit.submodule.SubmoduleStatus @Composable diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Tags.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Tags.kt deleted file mode 100644 index ffb9e89..0000000 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Tags.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.jetpackduba.gitnuro.ui - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.res.painterResource -import com.jetpackduba.gitnuro.extensions.simpleName -import com.jetpackduba.gitnuro.ui.components.SideMenuPanel -import com.jetpackduba.gitnuro.ui.components.SideMenuSubentry -import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel -import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu -import com.jetpackduba.gitnuro.ui.context_menu.tagContextMenuItems -import com.jetpackduba.gitnuro.viewmodels.TagsViewModel -import org.eclipse.jgit.lib.Ref - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun Tags( - tagsViewModel: TagsViewModel = gitnuroViewModel(), -) { - val tagsState = tagsViewModel.tags.collectAsState() - val tags = tagsState.value - val isExpanded by tagsViewModel.isExpanded.collectAsState() - - SideMenuPanel( - title = "Tags", - items = tags, - icon = painterResource("tag.svg"), - isExpanded = isExpanded, - onExpand = { tagsViewModel.onExpand() }, - itemContent = { tag -> - TagRow( - tag = tag, - onTagClicked = { tagsViewModel.selectTag(tag) }, - onCheckoutTag = { tagsViewModel.checkoutRef(tag) }, - onDeleteTag = { tagsViewModel.deleteTag(tag) } - ) - } - ) -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun TagRow( - tag: Ref, - onTagClicked: () -> Unit, - onCheckoutTag: () -> Unit, - onDeleteTag: () -> Unit, -) { - ContextMenu( - items = { - tagContextMenuItems( - onCheckoutTag = onCheckoutTag, - onDeleteTag = onDeleteTag, - ) - } - ) { - SideMenuSubentry( - text = tag.simpleName, - iconResourcePath = "tag.svg", - onClick = onTagClicked, - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/AdjustableOutlinedTextField.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/AdjustableOutlinedTextField.kt index 495c7d8..287fe55 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/AdjustableOutlinedTextField.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/AdjustableOutlinedTextField.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.jetpackduba.gitnuro.theme.onBackgroundSecondary import com.jetpackduba.gitnuro.theme.outlinedTextFieldColors @@ -46,6 +47,7 @@ fun AdjustableOutlinedTextField( shape: Shape = RoundedCornerShape(4.dp), backgroundColor: Color = MaterialTheme.colors.background, visualTransformation: VisualTransformation = VisualTransformation.None, + leadingIcon: @Composable (() -> Unit)? = null, ) { val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value @@ -60,7 +62,7 @@ fun AdjustableOutlinedTextField( ) { BasicTextField( modifier = Modifier - .heightIn(min = 40.dp) + .heightIn(min = 38.dp) .background(backgroundColor) .fillMaxWidth(), value = value, @@ -74,29 +76,44 @@ fun AdjustableOutlinedTextField( singleLine = singleLine, visualTransformation = visualTransformation, decorationBox = { innerTextField -> - Box( + Row( modifier = Modifier .border( - width = 1.dp, + width = 2.dp, color = indicatorColor, shape = shape ) .padding(horizontal = 12.dp), - contentAlignment = Alignment.CenterStart, + verticalAlignment = Alignment.CenterVertically, ) { + if (leadingIcon != null) { + leadingIcon() + Spacer(modifier = Modifier.width(8.dp)) + } innerTextField() } } ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxSize() + .padding(start = 12.dp), + ) { + if (leadingIcon != null) { + leadingIcon() + Spacer(modifier = Modifier.width(8.dp)) + } - if (value.isEmpty() && hint.isNotEmpty()) { - Text( - hint, - color = MaterialTheme.colors.onBackgroundSecondary, - modifier = Modifier - .padding(start = 12.dp, top = 12.dp), - style = MaterialTheme.typography.body2 - ) + if (value.isEmpty() && hint.isNotEmpty()) { + Text( + hint, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colors.onBackgroundSecondary, + style = MaterialTheme.typography.body2 + ) + } } } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/SideMenuEntry.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/SideMenuEntry.kt index 14531db..1dc8fd6 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/SideMenuEntry.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/SideMenuEntry.kt @@ -17,15 +17,17 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.jetpackduba.gitnuro.extensions.handMouseClickable import com.jetpackduba.gitnuro.theme.onBackgroundSecondary @Composable -fun SideMenuEntry( +fun SideMenuHeader( text: String, icon: Painter? = null, itemsCount: Int, isExpanded: Boolean, + onExpand: () -> Unit = {}, hoverIcon: @Composable (() -> Unit)? = null, ) { val hoverInteraction = remember { MutableInteractionSource() } @@ -35,7 +37,8 @@ fun SideMenuEntry( modifier = Modifier .height(36.dp) .fillMaxWidth() - .hoverable(hoverInteraction), + .hoverable(hoverInteraction) + .handMouseClickable { onExpand() }, verticalAlignment = Alignment.CenterVertically, ) { Icon( diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/SideMenuPanel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/SideMenuPanel.kt index daa5322..ea88caf 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/SideMenuPanel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/SideMenuPanel.kt @@ -26,7 +26,7 @@ fun SideMenuPanel( ContextMenu( items = contextItems ) { - SideMenuEntry( + SideMenuHeader( text = title, icon = icon, itemsCount = items.count(), diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/EditRemotesDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/EditRemotesDialog.kt index 8b3ed94..1d5f1b2 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/EditRemotesDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/EditRemotesDialog.kt @@ -4,7 +4,10 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.* +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear import androidx.compose.runtime.* @@ -16,12 +19,11 @@ import androidx.compose.ui.unit.dp import com.jetpackduba.gitnuro.extensions.handMouseClickable import com.jetpackduba.gitnuro.extensions.handOnHover import com.jetpackduba.gitnuro.theme.backgroundSelected -import com.jetpackduba.gitnuro.theme.outlinedTextFieldColors import com.jetpackduba.gitnuro.theme.onBackgroundSecondary -import com.jetpackduba.gitnuro.theme.textButtonColors +import com.jetpackduba.gitnuro.theme.outlinedTextFieldColors import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField import com.jetpackduba.gitnuro.ui.components.PrimaryButton -import com.jetpackduba.gitnuro.viewmodels.RemotesViewModel +import com.jetpackduba.gitnuro.viewmodels.sidepanel.RemotesViewModel import org.eclipse.jgit.transport.RemoteConfig @Composable @@ -38,7 +40,8 @@ fun EditRemotesDialog( ) } - val remotes by remotesViewModel.remotes.collectAsState() + val remotesState by remotesViewModel.remoteState.collectAsState() + val remotes = remotesState.remotes var remoteChanged by remember { mutableStateOf(false) } val selectedRemote = remotesEditorData.selectedRemote @@ -343,7 +346,7 @@ data class RemoteWrapper( ) { val haveUrisChanged: Boolean = isNew || fetchUri != originalFetchUri || - pushUri.toString() != originalPushUri + pushUri != originalPushUri } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModelsHolder.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModelsHolder.kt index 2447adf..6595ed8 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModelsHolder.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModelsHolder.kt @@ -1,22 +1,19 @@ package com.jetpackduba.gitnuro.viewmodels import com.jetpackduba.gitnuro.di.TabScope +import com.jetpackduba.gitnuro.viewmodels.sidepanel.* import javax.inject.Inject import javax.inject.Provider @TabScope class TabViewModelsHolder @Inject constructor( logViewModel: LogViewModel, - branchesViewModel: BranchesViewModel, - tagsViewModel: TagsViewModel, - remotesViewModel: RemotesViewModel, statusViewModel: StatusViewModel, menuViewModel: MenuViewModel, - stashesViewModel: StashesViewModel, - submodulesViewModel: SubmodulesViewModel, commitChangesViewModel: CommitChangesViewModel, cloneViewModel: CloneViewModel, settingsViewModel: SettingsViewModel, + sidePanelViewModel: SidePanelViewModel, // Dynamic VM private val diffViewModelProvider: Provider, private val rebaseInteractiveViewModelProvider: Provider, @@ -25,13 +22,9 @@ class TabViewModelsHolder @Inject constructor( ) { val viewModels = mapOf( logViewModel::class to logViewModel, - branchesViewModel::class to branchesViewModel, - tagsViewModel::class to tagsViewModel, - remotesViewModel::class to remotesViewModel, + sidePanelViewModel::class to sidePanelViewModel, statusViewModel::class to statusViewModel, menuViewModel::class to menuViewModel, - stashesViewModel::class to stashesViewModel, - submodulesViewModel::class to submodulesViewModel, commitChangesViewModel::class to commitChangesViewModel, cloneViewModel::class to cloneViewModel, settingsViewModel::class to settingsViewModel, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/BranchesViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/BranchesViewModel.kt similarity index 77% rename from src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/BranchesViewModel.kt rename to src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/BranchesViewModel.kt index 884f4d4..17abada 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/BranchesViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/BranchesViewModel.kt @@ -1,5 +1,7 @@ -package com.jetpackduba.gitnuro.viewmodels +package com.jetpackduba.gitnuro.viewmodels.sidepanel +import com.jetpackduba.gitnuro.extensions.lowercaseContains +import com.jetpackduba.gitnuro.extensions.simpleName import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.branches.* @@ -7,9 +9,10 @@ import com.jetpackduba.gitnuro.git.rebase.RebaseBranchUseCase import com.jetpackduba.gitnuro.git.remote_operations.PullFromSpecificBranchUseCase import com.jetpackduba.gitnuro.git.remote_operations.PushToSpecificBranchUseCase import com.jetpackduba.gitnuro.preferences.AppSettings +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.Ref @@ -17,7 +20,8 @@ import javax.inject.Inject private const val TAG = "BranchesViewModel" -class BranchesViewModel @Inject constructor( + +class BranchesViewModel @AssistedInject constructor( private val rebaseBranchUseCase: RebaseBranchUseCase, private val tabState: TabState, private val appSettings: AppSettings, @@ -30,16 +34,26 @@ class BranchesViewModel @Inject constructor( private val deleteBranchUseCase: DeleteBranchUseCase, private val checkoutRefUseCase: CheckoutRefUseCase, private val tabScope: CoroutineScope, -) : ExpandableViewModel(true) { + @Assisted + private val filter: StateFlow +) : SidePanelChildViewModel(true) { private val _branches = MutableStateFlow>(listOf()) - val branches: StateFlow> - get() = _branches - private val _currentBranch = MutableStateFlow(null) - val currentBranch: StateFlow - get() = _currentBranch + + val branchesState = combine(_branches, _currentBranch, isExpanded, filter) { branches, currentBranch, isExpanded, filter -> + BranchesState( + branches = branches.filter { it.simpleName.lowercaseContains(filter) }, + isExpanded = isExpanded, + currentBranch = currentBranch + ) + }.stateIn( + scope = tabScope, + started = SharingStarted.Eagerly, + initialValue = BranchesState(emptyList(), isExpanded.value, null) + ) init { + tabScope.launch { tabState.refreshFlowFiltered(RefreshType.ALL_DATA) { @@ -116,4 +130,10 @@ class BranchesViewModel @Inject constructor( remoteBranch = branch, ) } -} \ No newline at end of file +} + +data class BranchesState( + val branches: List, + val isExpanded: Boolean, + val currentBranch: Ref?, +) \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RemotesViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/RemotesViewModel.kt similarity index 69% rename from src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RemotesViewModel.kt rename to src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/RemotesViewModel.kt index cbf7a21..5dc7a72 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RemotesViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/RemotesViewModel.kt @@ -1,6 +1,8 @@ -package com.jetpackduba.gitnuro.viewmodels +package com.jetpackduba.gitnuro.viewmodels.sidepanel import com.jetpackduba.gitnuro.exceptions.InvalidRemoteUrlException +import com.jetpackduba.gitnuro.extensions.lowercaseContains +import com.jetpackduba.gitnuro.extensions.simpleName import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.branches.DeleteLocallyRemoteBranchesUseCase @@ -8,18 +10,18 @@ import com.jetpackduba.gitnuro.git.branches.GetRemoteBranchesUseCase import com.jetpackduba.gitnuro.git.remote_operations.DeleteRemoteBranchUseCase import com.jetpackduba.gitnuro.git.remotes.* import com.jetpackduba.gitnuro.ui.dialogs.RemoteWrapper +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.RemoteSetUrlCommand import org.eclipse.jgit.lib.Ref -import javax.inject.Inject -class RemotesViewModel @Inject constructor( +class RemotesViewModel @AssistedInject constructor( private val tabState: TabState, private val deleteRemoteBranchUseCase: DeleteRemoteBranchUseCase, private val getRemoteBranchesUseCase: GetRemoteBranchesUseCase, @@ -29,15 +31,37 @@ class RemotesViewModel @Inject constructor( private val updateRemoteUseCase: UpdateRemoteUseCase, private val deleteLocallyRemoteBranchesUseCase: DeleteLocallyRemoteBranchesUseCase, private val tabScope: CoroutineScope, -) : ExpandableViewModel() { - private val _remotes = MutableStateFlow>(listOf()) - val remotes: StateFlow> - get() = _remotes + @Assisted + private val filter: StateFlow +) : SidePanelChildViewModel(false) { + private val remotes = MutableStateFlow>(listOf()) + + val remoteState: StateFlow = combine(remotes, isExpanded, filter) { remotes, isExpanded, filter -> + val remotesFiltered = remotes.map { remote -> + val remoteInfo = remote.remoteInfo + + val newRemoteInfo = remoteInfo.copy( + branchesList = remoteInfo.branchesList.filter { branch -> + branch.simpleName.lowercaseContains(filter) + } + ) + + remote.copy(remoteInfo = newRemoteInfo) + } + + RemotesState( + remotesFiltered, + isExpanded + ) + }.stateIn( + scope = tabScope, + started = SharingStarted.Eagerly, + initialValue = RemotesState(emptyList(), isExpanded.value) + ) init { tabScope.launch { - tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REMOTES) - { + tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REMOTES) { refresh(tabState.git) } } @@ -61,7 +85,7 @@ class RemotesViewModel @Inject constructor( RemoteView(remoteInfo, true) } - _remotes.value = remoteViewList + this@RemotesViewModel.remotes.value = remoteViewList } fun deleteRemoteBranch(ref: Ref) = tabState.safeProcessing( @@ -74,14 +98,19 @@ class RemotesViewModel @Inject constructor( loadRemotes(git) } - fun onRemoteClicked(remoteInfo: RemoteView) { - val remotes = _remotes.value - val newRemoteInfo = remoteInfo.copy(isExpanded = !remoteInfo.isExpanded) - val newRemotesList = remotes.toMutableList() - val indexToReplace = newRemotesList.indexOf(remoteInfo) - newRemotesList[indexToReplace] = newRemoteInfo + fun onRemoteClicked(remoteClicked: RemoteView) { + val remoteName = remoteClicked.remoteInfo.remoteConfig.name + val remotes = this.remotes.value + val remoteInfo = remotes.firstOrNull { it.remoteInfo.remoteConfig.name == remoteName } - _remotes.value = newRemotesList + if(remoteInfo != null) { + val newRemoteInfo = remoteInfo.copy(isExpanded = !remoteClicked.isExpanded) + val newRemotesList = remotes.toMutableList() + val indexToReplace = newRemotesList.indexOf(remoteInfo) + newRemotesList[indexToReplace] = newRemoteInfo + + this.remotes.value = newRemotesList + } } fun selectBranch(ref: Ref) { @@ -151,4 +180,6 @@ class RemotesViewModel @Inject constructor( } } -data class RemoteView(val remoteInfo: RemoteInfo, val isExpanded: Boolean) \ No newline at end of file +data class RemoteView(val remoteInfo: RemoteInfo, val isExpanded: Boolean) + +data class RemotesState(val remotes: List, val isExpanded: Boolean) \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/ExpandableViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/SidePanelChildViewModel.kt similarity index 70% rename from src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/ExpandableViewModel.kt rename to src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/SidePanelChildViewModel.kt index 9fdd39c..cf7db4e 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/ExpandableViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/SidePanelChildViewModel.kt @@ -1,9 +1,9 @@ -package com.jetpackduba.gitnuro.viewmodels +package com.jetpackduba.gitnuro.viewmodels.sidepanel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -abstract class ExpandableViewModel(expandedDefault: Boolean = false) { +abstract class SidePanelChildViewModel(expandedDefault: Boolean) { private val _isExpanded = MutableStateFlow(expandedDefault) val isExpanded: StateFlow = _isExpanded diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/SidePanelViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/SidePanelViewModel.kt new file mode 100644 index 0000000..f2d9d10 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/SidePanelViewModel.kt @@ -0,0 +1,28 @@ +package com.jetpackduba.gitnuro.viewmodels.sidepanel + +import com.jetpackduba.gitnuro.di.factories.BranchesViewModelFactory +import com.jetpackduba.gitnuro.di.factories.RemotesViewModelFactory +import com.jetpackduba.gitnuro.di.factories.StashesViewModelFactory +import com.jetpackduba.gitnuro.di.factories.TagsViewModelFactory +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +class SidePanelViewModel @Inject constructor( + branchesViewModelFactory: BranchesViewModelFactory, + remotesViewModelFactory: RemotesViewModelFactory, + tagsViewModelFactory: TagsViewModelFactory, + stashesViewModelFactory: StashesViewModelFactory, +) { + private val _filter = MutableStateFlow("") + val filter: StateFlow = _filter + + val branchesViewModel: BranchesViewModel = branchesViewModelFactory.create(filter) + val remotesViewModel: RemotesViewModel = remotesViewModelFactory.create(filter) + val tagsViewModel: TagsViewModel = tagsViewModelFactory.create(filter) + val stashesViewModel: StashesViewModel = stashesViewModelFactory.create(filter) + + fun newFilter(newValue: String) { + _filter.value = newValue + } +} diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StashesViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/StashesViewModel.kt similarity index 70% rename from src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StashesViewModel.kt rename to src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/StashesViewModel.kt index 1948acf..5e1a1de 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StashesViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/StashesViewModel.kt @@ -1,5 +1,6 @@ -package com.jetpackduba.gitnuro.viewmodels +package com.jetpackduba.gitnuro.viewmodels.sidepanel +import com.jetpackduba.gitnuro.extensions.lowercaseContains import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.stash.ApplyStashUseCase @@ -7,26 +8,36 @@ import com.jetpackduba.gitnuro.git.stash.DeleteStashUseCase import com.jetpackduba.gitnuro.git.stash.GetStashListUseCase import com.jetpackduba.gitnuro.git.stash.PopStashUseCase import com.jetpackduba.gitnuro.ui.SelectedItem +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import org.eclipse.jgit.api.Git import org.eclipse.jgit.revwalk.RevCommit -import javax.inject.Inject -class StashesViewModel @Inject constructor( +class StashesViewModel @AssistedInject constructor( private val getStashListUseCase: GetStashListUseCase, private val applyStashUseCase: ApplyStashUseCase, private val popStashUseCase: PopStashUseCase, private val deleteStashUseCase: DeleteStashUseCase, private val tabState: TabState, private val tabScope: CoroutineScope, -) : ExpandableViewModel(true) { - private val _stashStatus = MutableStateFlow(StashStatus.Loaded(listOf())) + @Assisted + private val filter: StateFlow, +) : SidePanelChildViewModel(true) { + private val stashes = MutableStateFlow>(emptyList()) - val stashStatus: StateFlow - get() = _stashStatus + val stashesState: StateFlow = combine(stashes, isExpanded, filter) { stashes, isExpanded, filter -> + StashesState( + stashes = stashes.filter { it.fullMessage.lowercaseContains(filter) }, + isExpanded, + ) + }.stateIn( + tabScope, + SharingStarted.Eagerly, + StashesState(emptyList(), isExpanded.value) + ) init { tabScope.launch { @@ -41,9 +52,8 @@ class StashesViewModel @Inject constructor( } suspend fun loadStashes(git: Git) { - _stashStatus.value = StashStatus.Loading val stashList = getStashListUseCase(git) - _stashStatus.value = StashStatus.Loaded(stashList.toList()) + stashes.value = stashList } suspend fun refresh(git: Git) { @@ -74,7 +84,7 @@ class StashesViewModel @Inject constructor( stashDropped(stash) } - fun selectTab(stash: RevCommit) = tabState.runOperation( + fun selectStash(stash: RevCommit) = tabState.runOperation( refreshType = RefreshType.NONE, ) { tabState.newSelectedStash(stash) @@ -94,7 +104,4 @@ class StashesViewModel @Inject constructor( } -sealed class StashStatus { - object Loading : StashStatus() - data class Loaded(val stashes: List) : StashStatus() -} \ No newline at end of file +data class StashesState(val stashes: List, val isExpanded: Boolean) \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SubmodulesViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/SubmodulesViewModel.kt similarity index 95% rename from src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SubmodulesViewModel.kt rename to src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/SubmodulesViewModel.kt index 17057c9..8e97da4 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SubmodulesViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/SubmodulesViewModel.kt @@ -1,4 +1,4 @@ -package com.jetpackduba.gitnuro.viewmodels +package com.jetpackduba.gitnuro.viewmodels.sidepanel import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.TabState @@ -19,7 +19,7 @@ class SubmodulesViewModel @Inject constructor( private val initializeSubmoduleUseCase: InitializeSubmoduleUseCase, private val updateSubmoduleUseCase: UpdateSubmoduleUseCase, private val tabScope: CoroutineScope, -) : ExpandableViewModel() { +) : SidePanelChildViewModel(true) { private val _submodules = MutableStateFlow>>(listOf()) val submodules: StateFlow>> get() = _submodules diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TagsViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/TagsViewModel.kt similarity index 61% rename from src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TagsViewModel.kt rename to src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/TagsViewModel.kt index ecb6be8..8a493d6 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TagsViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/TagsViewModel.kt @@ -1,30 +1,43 @@ -package com.jetpackduba.gitnuro.viewmodels +package com.jetpackduba.gitnuro.viewmodels.sidepanel +import com.jetpackduba.gitnuro.extensions.lowercaseContains +import com.jetpackduba.gitnuro.extensions.simpleName import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.branches.CheckoutRefUseCase import com.jetpackduba.gitnuro.git.tags.DeleteTagUseCase import com.jetpackduba.gitnuro.git.tags.GetTagsUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.Ref -import javax.inject.Inject -class TagsViewModel @Inject constructor( +class TagsViewModel @AssistedInject constructor( private val tabState: TabState, private val getTagsUseCase: GetTagsUseCase, private val deleteTagUseCase: DeleteTagUseCase, private val checkoutRefUseCase: CheckoutRefUseCase, private val tabScope: CoroutineScope, -) : ExpandableViewModel() { - private val _tags = MutableStateFlow>(listOf()) - val tags: StateFlow> - get() = _tags + @Assisted + private val filter: StateFlow +) : SidePanelChildViewModel(false) { + private val tags = MutableStateFlow>(listOf()) + + val tagsState: StateFlow = combine(tags, isExpanded, filter) { tags, isExpanded, filter -> + TagsState( + tags.filter { tag -> tag.simpleName.lowercaseContains(filter) }, + isExpanded, + ) + }.stateIn( + scope = tabScope, + started = SharingStarted.Eagerly, + initialValue = TagsState(emptyList(), isExpanded.value) + ) init { tabScope.launch { @@ -38,7 +51,7 @@ class TagsViewModel @Inject constructor( private suspend fun loadTags(git: Git) = withContext(Dispatchers.IO) { val tagsList = getTagsUseCase(git) - _tags.value = tagsList + tags.value = tagsList } fun checkoutRef(ref: Ref) = tabState.safeProcessing( @@ -60,4 +73,6 @@ class TagsViewModel @Inject constructor( suspend fun refresh(git: Git) { loadTags(git) } -} \ No newline at end of file +} + +data class TagsState(val tags: List, val isExpanded: Boolean) \ No newline at end of file diff --git a/src/main/resources/search.svg b/src/main/resources/search.svg new file mode 100644 index 0000000..1465559 --- /dev/null +++ b/src/main/resources/search.svg @@ -0,0 +1 @@ + \ No newline at end of file