diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/extensions/BranchFilters.kt b/src/main/kotlin/com/jetpackduba/gitnuro/extensions/BranchFilters.kt new file mode 100644 index 0000000..7f189da --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/extensions/BranchFilters.kt @@ -0,0 +1,34 @@ +package com.jetpackduba.gitnuro.extensions + +import org.eclipse.jgit.lib.Ref + +/** + * Predicate for filtering branches + */ +typealias BranchFilter = (ref: Ref) -> Boolean + +fun Ref.matchesAll(filters: List): Boolean { + return filters.all { filter -> filter(this) } +} + +/** + * Matches only branches from the specified remote name. + */ +class OriginFilter( + private val remoteName: String +) : BranchFilter { + override fun invoke(ref: Ref): Boolean { + return ref.name.startsWith("refs/remotes/$remoteName") + } +} + +/** + * Matches only branches that contain a specific keyword in its name. + */ +class BranchNameContainsFilter( + private val keyword: String +) : BranchFilter { + override fun invoke(ref: Ref): Boolean { + return ref.name.contains(keyword) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/TabState.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/TabState.kt index de4e4b9..ace8a59 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/TabState.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/TabState.kt @@ -24,6 +24,8 @@ class TabState @Inject constructor( val selectedItem: StateFlow = _selectedItem private val _taskEvent = MutableSharedFlow() val taskEvent: SharedFlow = _taskEvent + private val _branchFilterKeyword = MutableStateFlow("") + val branchFilterKeyword: StateFlow = _branchFilterKeyword private var unsafeGit: Git? = null val git: Git @@ -160,6 +162,10 @@ class TabState @Inject constructor( } } + fun newBranchFilter(keyword: String) = runOperation(refreshType = RefreshType.BRANCH_FILTER) { + _branchFilterKeyword.value = keyword + } + private fun findCommit(git: Git, objectId: ObjectId): RevCommit { return git.repository.parseCommit(objectId) } @@ -202,6 +208,7 @@ enum class RefreshType { UNCOMMITED_CHANGES, UNCOMMITED_CHANGES_AND_LOG, REMOTES, + BRANCH_FILTER } enum class Processing { diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/BranchFilter.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/BranchFilter.kt new file mode 100644 index 0000000..fe5bebc --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/BranchFilter.kt @@ -0,0 +1,43 @@ +package com.jetpackduba.gitnuro.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +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.unit.dp +import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField +import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel +import com.jetpackduba.gitnuro.viewmodels.BranchFilterViewModel + +@Composable +fun BranchFilter( + branchFilterViewModel: BranchFilterViewModel = gitnuroViewModel() +) { + val filterKeyword by branchFilterViewModel.keyword.collectAsState() + + Box( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(16.dp) + ) { + AdjustableOutlinedTextField( + value = filterKeyword, + onValueChange = { branchFilterViewModel.newBranchFilter(keyword = it) }, + hint = "Filter Branches", + textStyle = MaterialTheme.typography.caption, + leadingIconResourcePath = "branch_filter.svg", + maxLines = 1, + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .heightIn(min = 8.dp) + .defaultMinSize(minHeight = 8.dp), + ) + } + +} \ 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..32e23a7 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt @@ -303,6 +303,7 @@ fun MainContentView( modifier = Modifier .weight(1f), ) { + BranchFilter() Branches() Remotes() Tags() 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..4fd3b7c 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/AdjustableOutlinedTextField.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/AdjustableOutlinedTextField.kt @@ -7,10 +7,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.TextFieldColors +import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -20,6 +17,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp @@ -46,6 +44,7 @@ fun AdjustableOutlinedTextField( shape: Shape = RoundedCornerShape(4.dp), backgroundColor: Color = MaterialTheme.colors.background, visualTransformation: VisualTransformation = VisualTransformation.None, + leadingIconResourcePath: String = "", ) { val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value @@ -84,19 +83,38 @@ fun AdjustableOutlinedTextField( .padding(horizontal = 12.dp), contentAlignment = Alignment.CenterStart, ) { - innerTextField() + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.wrapContentHeight(), + ) { + if (leadingIconResourcePath.isNotEmpty()) { + Icon( + painter = painterResource(leadingIconResourcePath), + contentDescription = null, + modifier = Modifier + .padding(end = 8.dp) + .size(16.dp), + tint = textColor, + ) + } + + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.wrapContentHeight(), + ) { + innerTextField() + if (value.isEmpty() && hint.isNotEmpty()) { + Text( + hint, + style = textStyle.copy(color = MaterialTheme.colors.onBackgroundSecondary) + ) + } + } + } } } ) - if (value.isEmpty() && hint.isNotEmpty()) { - Text( - hint, - color = MaterialTheme.colors.onBackgroundSecondary, - modifier = Modifier - .padding(start = 12.dp, top = 12.dp), - style = MaterialTheme.typography.body2 - ) - } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/BranchFilterViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/BranchFilterViewModel.kt new file mode 100644 index 0000000..251814b --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/BranchFilterViewModel.kt @@ -0,0 +1,15 @@ +package com.jetpackduba.gitnuro.viewmodels + +import com.jetpackduba.gitnuro.git.TabState +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +class BranchFilterViewModel @Inject constructor( + private val tabState: TabState +) { + val keyword: StateFlow = tabState.branchFilterKeyword + + fun newBranchFilter(keyword: String) { + tabState.newBranchFilter(keyword) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/BranchesViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/BranchesViewModel.kt index 884f4d4..05373c0 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/BranchesViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/BranchesViewModel.kt @@ -1,5 +1,6 @@ package com.jetpackduba.gitnuro.viewmodels +import com.jetpackduba.gitnuro.extensions.BranchNameContainsFilter import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.branches.* @@ -41,8 +42,7 @@ class BranchesViewModel @Inject constructor( init { tabScope.launch { - tabState.refreshFlowFiltered(RefreshType.ALL_DATA) - { + tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.BRANCH_FILTER) { refresh(tabState.git) } } @@ -51,7 +51,11 @@ class BranchesViewModel @Inject constructor( private suspend fun loadBranches(git: Git) { _currentBranch.value = getCurrentBranchUseCase(git) - val branchesList = getBranchesUseCase(git).toMutableList() + val branchNameFilter = BranchNameContainsFilter(keyword = tabState.branchFilterKeyword.value) + + val branchesList = getBranchesUseCase(git) + .filter(branchNameFilter) + .toMutableList() // set selected branch as the first one always val selectedBranch = branchesList.find { it.name == _currentBranch.value?.name } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RemotesViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RemotesViewModel.kt index cbf7a21..33b5dd5 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RemotesViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RemotesViewModel.kt @@ -1,6 +1,9 @@ package com.jetpackduba.gitnuro.viewmodels import com.jetpackduba.gitnuro.exceptions.InvalidRemoteUrlException +import com.jetpackduba.gitnuro.extensions.BranchNameContainsFilter +import com.jetpackduba.gitnuro.extensions.OriginFilter +import com.jetpackduba.gitnuro.extensions.matchesAll import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.branches.DeleteLocallyRemoteBranchesUseCase @@ -36,8 +39,7 @@ class RemotesViewModel @Inject constructor( init { tabScope.launch { - tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REMOTES) - { + tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REMOTES, RefreshType.BRANCH_FILTER) { refresh(tabState.git) } } @@ -51,9 +53,14 @@ class RemotesViewModel @Inject constructor( getRemotesUseCase(git, allRemoteBranches) val remoteInfoList = remotes.map { remoteConfig -> - val remoteBranches = allRemoteBranches.filter { branch -> - branch.name.startsWith("refs/remotes/${remoteConfig.name}") - } + val filters = listOf( + OriginFilter(remoteName = remoteConfig.name), + BranchNameContainsFilter(keyword = tabState.branchFilterKeyword.value) + ) + + val remoteBranches = allRemoteBranches + .filter { it.matchesAll(filters) } + RemoteInfo(remoteConfig, remoteBranches) } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt index 7470ceb..da6894e 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt @@ -63,6 +63,7 @@ class TabViewModel @Inject constructor( val errorsManager: ErrorsManager = tabState.errorsManager val selectedItem: StateFlow = tabState.selectedItem var diffViewModel: DiffViewModel? = null + val branchFilterKeyword: StateFlow = tabState.branchFilterKeyword var rebaseInteractiveViewModel: RebaseInteractiveViewModel? = null private set diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModelsHolder.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModelsHolder.kt index 2447adf..41af668 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModelsHolder.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModelsHolder.kt @@ -17,6 +17,7 @@ class TabViewModelsHolder @Inject constructor( commitChangesViewModel: CommitChangesViewModel, cloneViewModel: CloneViewModel, settingsViewModel: SettingsViewModel, + branchFilterViewModel: BranchFilterViewModel, // Dynamic VM private val diffViewModelProvider: Provider, private val rebaseInteractiveViewModelProvider: Provider, @@ -35,6 +36,7 @@ class TabViewModelsHolder @Inject constructor( commitChangesViewModel::class to commitChangesViewModel, cloneViewModel::class to cloneViewModel, settingsViewModel::class to settingsViewModel, + branchFilterViewModel::class to branchFilterViewModel, ) diff --git a/src/main/resources/branch_filter.svg b/src/main/resources/branch_filter.svg new file mode 100644 index 0000000..d769d06 --- /dev/null +++ b/src/main/resources/branch_filter.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file