feat: add branch filtering

This commit is contained in:
Aguragorn 2023-01-26 23:57:31 +08:00
parent 1d5085b689
commit 81d3c9450b
11 changed files with 157 additions and 22 deletions

View File

@ -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<BranchFilter>): 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)
}
}

View File

@ -24,6 +24,8 @@ class TabState @Inject constructor(
val selectedItem: StateFlow<SelectedItem> = _selectedItem val selectedItem: StateFlow<SelectedItem> = _selectedItem
private val _taskEvent = MutableSharedFlow<TaskEvent>() private val _taskEvent = MutableSharedFlow<TaskEvent>()
val taskEvent: SharedFlow<TaskEvent> = _taskEvent val taskEvent: SharedFlow<TaskEvent> = _taskEvent
private val _branchFilterKeyword = MutableStateFlow("")
val branchFilterKeyword: StateFlow<String> = _branchFilterKeyword
private var unsafeGit: Git? = null private var unsafeGit: Git? = null
val git: Git 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 { private fun findCommit(git: Git, objectId: ObjectId): RevCommit {
return git.repository.parseCommit(objectId) return git.repository.parseCommit(objectId)
} }
@ -202,6 +208,7 @@ enum class RefreshType {
UNCOMMITED_CHANGES, UNCOMMITED_CHANGES,
UNCOMMITED_CHANGES_AND_LOG, UNCOMMITED_CHANGES_AND_LOG,
REMOTES, REMOTES,
BRANCH_FILTER
} }
enum class Processing { enum class Processing {

View File

@ -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),
)
}
}

View File

@ -303,6 +303,7 @@ fun MainContentView(
modifier = Modifier modifier = Modifier
.weight(1f), .weight(1f),
) { ) {
BranchFilter()
Branches() Branches()
Remotes() Remotes()
Tags() Tags()

View File

@ -7,10 +7,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.LocalTextStyle import androidx.compose.material.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextFieldColors
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember 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.Shape
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -46,6 +44,7 @@ fun AdjustableOutlinedTextField(
shape: Shape = RoundedCornerShape(4.dp), shape: Shape = RoundedCornerShape(4.dp),
backgroundColor: Color = MaterialTheme.colors.background, backgroundColor: Color = MaterialTheme.colors.background,
visualTransformation: VisualTransformation = VisualTransformation.None, visualTransformation: VisualTransformation = VisualTransformation.None,
leadingIconResourcePath: String = "",
) { ) {
val textColor = textStyle.color.takeOrElse { val textColor = textStyle.color.takeOrElse {
colors.textColor(enabled).value colors.textColor(enabled).value
@ -84,19 +83,38 @@ fun AdjustableOutlinedTextField(
.padding(horizontal = 12.dp), .padding(horizontal = 12.dp),
contentAlignment = Alignment.CenterStart, 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
)
}
} }
} }

View File

@ -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<String> = tabState.branchFilterKeyword
fun newBranchFilter(keyword: String) {
tabState.newBranchFilter(keyword)
}
}

View File

@ -1,5 +1,6 @@
package com.jetpackduba.gitnuro.viewmodels package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.extensions.BranchNameContainsFilter
import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.branches.* import com.jetpackduba.gitnuro.git.branches.*
@ -41,8 +42,7 @@ class BranchesViewModel @Inject constructor(
init { init {
tabScope.launch { tabScope.launch {
tabState.refreshFlowFiltered(RefreshType.ALL_DATA) tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.BRANCH_FILTER) {
{
refresh(tabState.git) refresh(tabState.git)
} }
} }
@ -51,7 +51,11 @@ class BranchesViewModel @Inject constructor(
private suspend fun loadBranches(git: Git) { private suspend fun loadBranches(git: Git) {
_currentBranch.value = getCurrentBranchUseCase(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 // set selected branch as the first one always
val selectedBranch = branchesList.find { it.name == _currentBranch.value?.name } val selectedBranch = branchesList.find { it.name == _currentBranch.value?.name }

View File

@ -1,6 +1,9 @@
package com.jetpackduba.gitnuro.viewmodels package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.exceptions.InvalidRemoteUrlException 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.RefreshType
import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.branches.DeleteLocallyRemoteBranchesUseCase import com.jetpackduba.gitnuro.git.branches.DeleteLocallyRemoteBranchesUseCase
@ -36,8 +39,7 @@ class RemotesViewModel @Inject constructor(
init { init {
tabScope.launch { tabScope.launch {
tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REMOTES) tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REMOTES, RefreshType.BRANCH_FILTER) {
{
refresh(tabState.git) refresh(tabState.git)
} }
} }
@ -51,9 +53,14 @@ class RemotesViewModel @Inject constructor(
getRemotesUseCase(git, allRemoteBranches) getRemotesUseCase(git, allRemoteBranches)
val remoteInfoList = remotes.map { remoteConfig -> val remoteInfoList = remotes.map { remoteConfig ->
val remoteBranches = allRemoteBranches.filter { branch -> val filters = listOf(
branch.name.startsWith("refs/remotes/${remoteConfig.name}") OriginFilter(remoteName = remoteConfig.name),
} BranchNameContainsFilter(keyword = tabState.branchFilterKeyword.value)
)
val remoteBranches = allRemoteBranches
.filter { it.matchesAll(filters) }
RemoteInfo(remoteConfig, remoteBranches) RemoteInfo(remoteConfig, remoteBranches)
} }

View File

@ -63,6 +63,7 @@ class TabViewModel @Inject constructor(
val errorsManager: ErrorsManager = tabState.errorsManager val errorsManager: ErrorsManager = tabState.errorsManager
val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem
var diffViewModel: DiffViewModel? = null var diffViewModel: DiffViewModel? = null
val branchFilterKeyword: StateFlow<String> = tabState.branchFilterKeyword
var rebaseInteractiveViewModel: RebaseInteractiveViewModel? = null var rebaseInteractiveViewModel: RebaseInteractiveViewModel? = null
private set private set

View File

@ -17,6 +17,7 @@ class TabViewModelsHolder @Inject constructor(
commitChangesViewModel: CommitChangesViewModel, commitChangesViewModel: CommitChangesViewModel,
cloneViewModel: CloneViewModel, cloneViewModel: CloneViewModel,
settingsViewModel: SettingsViewModel, settingsViewModel: SettingsViewModel,
branchFilterViewModel: BranchFilterViewModel,
// Dynamic VM // Dynamic VM
private val diffViewModelProvider: Provider<DiffViewModel>, private val diffViewModelProvider: Provider<DiffViewModel>,
private val rebaseInteractiveViewModelProvider: Provider<RebaseInteractiveViewModel>, private val rebaseInteractiveViewModelProvider: Provider<RebaseInteractiveViewModel>,
@ -35,6 +36,7 @@ class TabViewModelsHolder @Inject constructor(
commitChangesViewModel::class to commitChangesViewModel, commitChangesViewModel::class to commitChangesViewModel,
cloneViewModel::class to cloneViewModel, cloneViewModel::class to cloneViewModel,
settingsViewModel::class to settingsViewModel, settingsViewModel::class to settingsViewModel,
branchFilterViewModel::class to branchFilterViewModel,
) )

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
<path d="M10 18v-2h4v2Zm-4-5v-2h12v2ZM3 8V6h18v2Z"/>
</svg>

After

Width:  |  Height:  |  Size: 127 B