feat: add branch filtering
This commit is contained in:
parent
1d5085b689
commit
81d3c9450b
@ -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)
|
||||
}
|
||||
}
|
@ -24,6 +24,8 @@ class TabState @Inject constructor(
|
||||
val selectedItem: StateFlow<SelectedItem> = _selectedItem
|
||||
private val _taskEvent = MutableSharedFlow<TaskEvent>()
|
||||
val taskEvent: SharedFlow<TaskEvent> = _taskEvent
|
||||
private val _branchFilterKeyword = MutableStateFlow("")
|
||||
val branchFilterKeyword: StateFlow<String> = _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 {
|
||||
|
43
src/main/kotlin/com/jetpackduba/gitnuro/ui/BranchFilter.kt
Normal file
43
src/main/kotlin/com/jetpackduba/gitnuro/ui/BranchFilter.kt
Normal 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),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -303,6 +303,7 @@ fun MainContentView(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
) {
|
||||
BranchFilter()
|
||||
Branches()
|
||||
Remotes()
|
||||
Tags()
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 }
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -63,6 +63,7 @@ class TabViewModel @Inject constructor(
|
||||
val errorsManager: ErrorsManager = tabState.errorsManager
|
||||
val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem
|
||||
var diffViewModel: DiffViewModel? = null
|
||||
val branchFilterKeyword: StateFlow<String> = tabState.branchFilterKeyword
|
||||
|
||||
var rebaseInteractiveViewModel: RebaseInteractiveViewModel? = null
|
||||
private set
|
||||
|
@ -17,6 +17,7 @@ class TabViewModelsHolder @Inject constructor(
|
||||
commitChangesViewModel: CommitChangesViewModel,
|
||||
cloneViewModel: CloneViewModel,
|
||||
settingsViewModel: SettingsViewModel,
|
||||
branchFilterViewModel: BranchFilterViewModel,
|
||||
// Dynamic VM
|
||||
private val diffViewModelProvider: Provider<DiffViewModel>,
|
||||
private val rebaseInteractiveViewModelProvider: Provider<RebaseInteractiveViewModel>,
|
||||
@ -35,6 +36,7 @@ class TabViewModelsHolder @Inject constructor(
|
||||
commitChangesViewModel::class to commitChangesViewModel,
|
||||
cloneViewModel::class to cloneViewModel,
|
||||
settingsViewModel::class to settingsViewModel,
|
||||
branchFilterViewModel::class to branchFilterViewModel,
|
||||
)
|
||||
|
||||
|
||||
|
3
src/main/resources/branch_filter.svg
Normal file
3
src/main/resources/branch_filter.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user