Added option to search files in (un)commited changes

Fixes #44
This commit is contained in:
Abdelilah El Aissaoui 2023-04-24 01:37:38 +02:00
parent 91094a8771
commit 9dfd5073bd
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
12 changed files with 509 additions and 76 deletions

View File

@ -17,6 +17,7 @@ object AppIcons {
const val COPY = "copy.svg"
const val CUT = "cut.svg"
const val DELETE = "delete.svg"
const val DONE = "done.svg"
const val DOWNLOAD = "download.svg"
const val DROPDOWN = "dropdown.svg"
const val ERROR = "error.svg"
@ -37,6 +38,7 @@ object AppIcons {
const val PERSON = "person.svg"
const val REFRESH = "refresh.svg"
const val REMOVE = "remove.svg"
const val REMOVE_DONE = "remove_done.svg"
const val REVERT = "revert.svg"
const val SEARCH = "search.svg"
const val SETTINGS = "settings.svg"

View File

@ -11,11 +11,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -26,7 +26,7 @@ import com.jetpackduba.gitnuro.theme.*
import com.jetpackduba.gitnuro.ui.components.*
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.commitedChangesEntriesContextMenuItems
import com.jetpackduba.gitnuro.viewmodels.CommitChangesStatus
import com.jetpackduba.gitnuro.viewmodels.CommitChangesState
import com.jetpackduba.gitnuro.viewmodels.CommitChangesViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -48,21 +48,36 @@ fun CommitChanges(
commitChangesViewModel.loadChanges(selectedItem.revCommit)
}
val commitChangesStatusState = commitChangesViewModel.commitChangesStatus.collectAsState()
val commitChangesStatus = commitChangesViewModel.commitChangesState.collectAsState().value
val showSearch by commitChangesViewModel.showSearch.collectAsState()
when (val commitChangesStatus = commitChangesStatusState.value) {
CommitChangesStatus.Loading -> {
var searchFilter by remember(commitChangesViewModel, showSearch, commitChangesStatus) {
mutableStateOf(commitChangesViewModel.searchFilter.value)
}
when (commitChangesStatus) {
CommitChangesState.Loading -> {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant)
}
is CommitChangesStatus.Loaded -> {
is CommitChangesState.Loaded -> {
CommitChangesView(
diffSelected = diffSelected,
commit = commitChangesStatus.commit,
changes = commitChangesStatus.changes,
changes = commitChangesStatus.changesFiltered,
onDiffSelected = onDiffSelected,
onBlame = onBlame,
onHistory = onHistory,
showSearch = showSearch,
searchFilter = searchFilter,
onSearchFilterToggled = { visible ->
commitChangesViewModel.onSearchFilterToggled(visible)
},
onSearchFilterChanged = { filter ->
searchFilter = filter
commitChangesViewModel.onSearchFilterChanged(filter)
},
)
}
}
@ -76,15 +91,23 @@ fun CommitChangesView(
diffSelected: DiffEntryType?,
onBlame: (String) -> Unit,
onHistory: (String) -> Unit,
showSearch: Boolean,
searchFilter: TextFieldValue,
onSearchFilterToggled: (Boolean) -> Unit,
onSearchFilterChanged: (TextFieldValue) -> Unit,
) {
/**
* State used to prevent the text field from getting the focus when returning from another tab
*/
var requestFocus by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.padding(end = 8.dp, bottom = 8.dp)
.fillMaxSize(),
) {
val scroll = rememberScrollState(0)
var showSearch by remember(commit) { mutableStateOf(false) }
var searchFilter by remember(commit) { mutableStateOf("") } // TODO Persist in viewmodel
val searchFocusRequester = remember { FocusRequester() }
Column(
@ -116,43 +139,40 @@ fun CommitChangesView(
IconButton(
onClick = {
showSearch = !showSearch
}
onSearchFilterToggled(!showSearch)
if (!showSearch)
requestFocus = true
},
modifier = Modifier.handOnHover(),
) {
Icon(
painter = painterResource(AppIcons.SEARCH),
contentDescription = null,
modifier = Modifier.size(16.dp),
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colors.onBackground,
)
}
}
if (showSearch) {
AdjustableOutlinedTextField(
value = searchFilter,
onValueChange = {
searchFilter = it
},
modifier = Modifier.fillMaxWidth()
.focusable()
.focusRequester(searchFocusRequester)
SearchTextField(
searchFilter = searchFilter,
onSearchFilterChanged = onSearchFilterChanged,
searchFocusRequester = searchFocusRequester,
)
}
LaunchedEffect(showSearch) {
if (showSearch) {
LaunchedEffect(showSearch, requestFocus) {
if (showSearch && requestFocus) {
searchFocusRequester.requestFocus()
requestFocus = false
}
}
CommitLogChanges(
diffSelected = diffSelected,
diffEntries = if (showSearch && searchFilter.isNotBlank()) changes.filter {
it.filePath.lowercaseContains(
searchFilter
)
} else changes,
diffEntries = changes,
onDiffSelected = onDiffSelected,
onBlame = onBlame,
onHistory = onHistory,

View File

@ -8,6 +8,7 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
@ -23,14 +24,19 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.extensions.*
import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.git.workspace.StatusEntry
@ -38,16 +44,14 @@ import com.jetpackduba.gitnuro.git.workspace.StatusType
import com.jetpackduba.gitnuro.keybindings.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.theme.*
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
import com.jetpackduba.gitnuro.ui.components.SecondaryButton
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.components.*
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement
import com.jetpackduba.gitnuro.ui.context_menu.EntryType
import com.jetpackduba.gitnuro.ui.context_menu.statusEntriesContextMenuItems
import com.jetpackduba.gitnuro.ui.dialogs.CommitAuthorDialog
import com.jetpackduba.gitnuro.viewmodels.CommitterDataRequestState
import com.jetpackduba.gitnuro.viewmodels.StageStatus
import com.jetpackduba.gitnuro.viewmodels.StageState
import com.jetpackduba.gitnuro.viewmodels.StatusViewModel
import org.eclipse.jgit.lib.RepositoryState
@ -61,7 +65,7 @@ fun UncommitedChanges(
onBlameFile: (String) -> Unit,
onHistoryFile: (String) -> Unit,
) {
val stageStatusState = statusViewModel.stageStatus.collectAsState()
val stageStatus = statusViewModel.stageState.collectAsState().value
var commitMessage by remember(statusViewModel) { mutableStateOf(statusViewModel.savedCommitMessage.message) }
val stagedListState by statusViewModel.stagedLazyListState.collectAsState()
val unstagedListState by statusViewModel.unstagedLazyListState.collectAsState()
@ -69,14 +73,18 @@ fun UncommitedChanges(
val committerDataRequestState = statusViewModel.committerDataRequestState.collectAsState()
val committerDataRequestStateValue = committerDataRequestState.value
val stageStatus = stageStatusState.value
val showSearchStaged by statusViewModel.showSearchStaged.collectAsState()
val searchFilterStaged by statusViewModel.searchFilterStaged.collectAsState()
val showSearchUnstaged by statusViewModel.showSearchUnstaged.collectAsState()
val searchFilterUnstaged by statusViewModel.searchFilterUnstaged.collectAsState()
val staged: List<StatusEntry>
val unstaged: List<StatusEntry>
val isLoading: Boolean
if (stageStatus is StageStatus.Loaded) {
staged = stageStatus.staged
unstaged = stageStatus.unstaged
if (stageStatus is StageState.Loaded) {
staged = stageStatus.stagedFiltered
unstaged = stageStatus.unstagedFiltered
isLoading = stageStatus.isPartiallyReloading
} else {
staged = listOf()
@ -133,12 +141,21 @@ fun UncommitedChanges(
title = "Staged",
allActionTitle = "Unstage all",
actionTitle = "Unstage",
actionIcon = AppIcons.REMOVE_DONE,
selectedEntryType = if (selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null,
actionColor = MaterialTheme.colors.error,
actionTextColor = MaterialTheme.colors.onError,
statusEntries = staged,
lazyListState = stagedListState,
onDiffEntrySelected = onStagedDiffEntrySelected,
showSearch = showSearchStaged,
searchFilter = searchFilterStaged,
onSearchFilterToggled = {
statusViewModel.onSearchFilterToggledStaged(it)
},
onSearchFilterChanged = {
statusViewModel.onSearchFilterChangedStaged(it)
},
onDiffEntryOptionSelected = {
statusViewModel.unstage(it)
},
@ -163,12 +180,21 @@ fun UncommitedChanges(
.fillMaxWidth(),
title = "Unstaged",
actionTitle = "Stage",
actionIcon = AppIcons.DONE,
selectedEntryType = if (selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null,
actionColor = MaterialTheme.colors.primary,
actionTextColor = MaterialTheme.colors.onPrimary,
statusEntries = unstaged,
lazyListState = unstagedListState,
onDiffEntrySelected = onUnstagedDiffEntrySelected,
showSearch = showSearchUnstaged,
searchFilter = searchFilterUnstaged,
onSearchFilterToggled = {
statusViewModel.onSearchFilterToggledUnstaged(it)
},
onSearchFilterChanged = {
statusViewModel.onSearchFilterChangedUnstaged(it)
},
onDiffEntryOptionSelected = {
statusViewModel.stage(it)
},
@ -558,6 +584,11 @@ private fun EntriesList(
actionTitle: String,
actionColor: Color,
actionTextColor: Color,
actionIcon: String,
showSearch: Boolean,
searchFilter: TextFieldValue,
onSearchFilterToggled: (Boolean) -> Unit,
onSearchFilterChanged: (TextFieldValue) -> Unit,
statusEntries: List<StatusEntry>,
lazyListState: LazyListState,
onDiffEntrySelected: (StatusEntry) -> Unit,
@ -567,19 +598,30 @@ private fun EntriesList(
allActionTitle: String,
selectedEntryType: DiffEntryType?,
) {
val searchFocusRequester = remember { FocusRequester() }
val headerHoverInteraction = remember { MutableInteractionSource() }
val isHeaderHovered by headerHoverInteraction.collectIsHoveredAsState()
/**
* State used to prevent the text field from getting the focus when returning from another tab
*/
var requestFocus by remember { mutableStateOf(false) }
Column(
modifier = modifier
) {
Box(
Row(
modifier = Modifier
.height(34.dp)
.fillMaxWidth()
.background(color = MaterialTheme.colors.tertiarySurface)
.hoverable(headerHoverInteraction),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 16.dp)
.fillMaxWidth(),
.padding(start = 16.dp, end = 8.dp)
.weight(1f),
text = title,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Left,
@ -588,15 +630,49 @@ private fun EntriesList(
maxLines = 1,
)
SecondaryButton(
modifier = Modifier.align(Alignment.CenterEnd),
IconButton(
onClick = {
onSearchFilterToggled(!showSearch)
if (!showSearch)
requestFocus = true
},
modifier = Modifier.handOnHover()
) {
Icon(
painter = painterResource(AppIcons.SEARCH),
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colors.onBackground,
)
}
SecondaryButtonCompactable(
text = allActionTitle,
icon = actionIcon,
isParentHovered = isHeaderHovered,
backgroundButton = actionColor,
textColor = actionTextColor,
onClick = onAllAction
onClick = onAllAction,
modifier = Modifier.padding(start = 4.dp, end = 16.dp),
)
}
if (showSearch) {
SearchTextField(
searchFilter = searchFilter,
onSearchFilterChanged = onSearchFilterChanged,
searchFocusRequester = searchFocusRequester,
)
}
LaunchedEffect(showSearch, requestFocus) {
if (showSearch && requestFocus) {
searchFocusRequester.requestFocus()
requestFocus = false
}
}
ScrollableLazyColumn(
modifier = Modifier
.fillMaxSize()

View File

@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -126,3 +127,103 @@ fun AdjustableOutlinedTextField(
}
}
}
@Composable
fun AdjustableOutlinedTextField(
value: TextFieldValue,
hint: String = "",
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
isError: Boolean = false,
singleLine: Boolean = false,
colors: TextFieldColors = outlinedTextFieldColors(),
maxLines: Int = Int.MAX_VALUE,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
textStyle: TextStyle = LocalTextStyle.current.copy(
fontSize = MaterialTheme.typography.body1.fontSize,
color = MaterialTheme.colors.onBackground,
),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = RoundedCornerShape(4.dp),
backgroundColor: Color = MaterialTheme.colors.background,
visualTransformation: VisualTransformation = VisualTransformation.None,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
) {
val textColor = textStyle.color.takeOrElse {
colors.textColor(enabled).value
}
val cursorColor = colors.cursorColor(isError).value
val indicatorColor by colors.indicatorColor(enabled, isError, interactionSource)
Box(
modifier = modifier
.height(IntrinsicSize.Min)
) {
BasicTextField(
modifier = Modifier
.heightIn(min = 38.dp)
.background(backgroundColor)
.fillMaxWidth(),
value = value,
onValueChange = onValueChange,
enabled = enabled,
maxLines = maxLines,
textStyle = textStyle.copy(color = textColor),
interactionSource = interactionSource,
keyboardOptions = keyboardOptions,
cursorBrush = SolidColor(cursorColor),
singleLine = singleLine,
visualTransformation = visualTransformation,
decorationBox = { innerTextField: @Composable () -> Unit ->
Row(
modifier = Modifier
.border(
width = 2.dp,
color = indicatorColor,
shape = shape
)
.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (leadingIcon != null) {
leadingIcon()
Spacer(modifier = Modifier.width(8.dp))
}
Box(modifier = Modifier.weight(1f)) {
innerTextField()
}
if (trailingIcon != null) {
Spacer(modifier = Modifier.width(8.dp))
trailingIcon()
}
}
}
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.padding(start = 12.dp),
) {
if (leadingIcon != null) {
leadingIcon()
Spacer(modifier = Modifier.width(8.dp))
}
if (value.text.isEmpty() && hint.isNotEmpty()) {
Text(
hint,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colors.onBackgroundSecondary,
style = MaterialTheme.typography.body2
)
}
}
}
}

View File

@ -146,7 +146,8 @@ fun RepositoriesTabPanel(
fun Tab(
title: MutableState<String>,
isSelected: Boolean,
onClick: () -> Unit, onCloseTab: () -> Unit
onClick: () -> Unit,
onCloseTab: () -> Unit,
) {
val backgroundColor = if (isSelected)
MaterialTheme.colors.surface

View File

@ -0,0 +1,38 @@
package com.jetpackduba.gitnuro.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
@Composable
fun SearchTextField(
searchFilter: TextFieldValue,
onSearchFilterChanged: (TextFieldValue) -> Unit,
searchFocusRequester: FocusRequester,
) {
Box(
modifier = Modifier
.background(MaterialTheme.colors.background)
.padding(horizontal = 4.dp, vertical = 4.dp)
) {
AdjustableOutlinedTextField(
value = searchFilter,
onValueChange = {
onSearchFilterChanged(it)
},
hint = "Search files by name or path",
modifier = Modifier.fillMaxWidth()
.focusable()
.focusRequester(searchFocusRequester)
)
}
}

View File

@ -1,17 +1,29 @@
package com.jetpackduba.gitnuro.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.theme.tertiarySurface
@Composable
fun SecondaryButton(
@ -24,8 +36,7 @@ fun SecondaryButton(
) {
Box(
modifier = modifier
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(5.dp))
.clip(RoundedCornerShape(4.dp))
.background(backgroundButton)
.handMouseClickable { onClick() },
) {
@ -38,3 +49,72 @@ fun SecondaryButton(
)
}
}
@Composable
fun SecondaryButtonCompactable(
modifier: Modifier = Modifier,
icon: String,
text: String,
textColor: Color = MaterialTheme.colors.onPrimary,
backgroundButton: Color,
maxLines: Int = 1,
isParentHovered: Boolean,
onClick: () -> Unit,
) {
val hoverInteraction = remember { MutableInteractionSource() }
val isHovered by hoverInteraction.collectIsHoveredAsState()
var isExpanded by remember { mutableStateOf(false) }
LaunchedEffect(isHovered, isParentHovered) {
isExpanded = when {
isHovered -> true
isExpanded && isParentHovered -> true
else -> false
}
}
val targetBackground: Color
val iconPadding: Float
if (isExpanded) {
targetBackground = backgroundButton
iconPadding = 12f
} else {
targetBackground = MaterialTheme.colors.tertiarySurface
iconPadding = 0f
}
val color by animateColorAsState(targetBackground)
val iconPaddingState by animateFloatAsState(iconPadding)
Row(
modifier = modifier
.clip(RoundedCornerShape(4.dp))
.background(color)
.hoverable(hoverInteraction)
.handMouseClickable { onClick() }
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painterResource(icon),
contentDescription = null,
tint = MaterialTheme.colors.onBackground,
modifier = Modifier
.padding(start = iconPaddingState.dp, end = 8.dp)
.size(18.dp)
)
AnimatedVisibility(
visible = isExpanded,
) {
Text(
text = text,
style = MaterialTheme.typography.body2,
color = textColor,
maxLines = maxLines,
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp, end = 12.dp)
)
}
}
}

View File

@ -626,13 +626,15 @@ fun HunkHeader(
text = "Discard hunk",
backgroundButton = MaterialTheme.colors.error,
textColor = MaterialTheme.colors.onError,
onClick = onResetHunk
onClick = onResetHunk,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
SecondaryButton(
text = buttonText,
backgroundButton = color,
modifier = Modifier.padding(horizontal = 16.dp),
onClick = {
if (diffEntryType is DiffEntryType.StagedDiff) {
onUnstageHunk()
@ -787,6 +789,7 @@ fun UncommitedDiffFileHeaderButtons(
SecondaryButton(
text = buttonText,
backgroundButton = color,
modifier = Modifier.padding(horizontal = 16.dp),
onClick = {
if (diffEntryType is DiffEntryType.StagedDiff) {
onUnstageFile(diffEntryType.statusEntry)

View File

@ -1,11 +1,14 @@
package com.jetpackduba.gitnuro.viewmodels
import androidx.compose.ui.text.input.TextFieldValue
import com.jetpackduba.gitnuro.extensions.delayedStateChange
import com.jetpackduba.gitnuro.extensions.filePath
import com.jetpackduba.gitnuro.extensions.lowercaseContains
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.diff.GetCommitDiffEntriesUseCase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
@ -15,26 +18,69 @@ private const val MIN_TIME_IN_MS_TO_SHOW_LOAD = 300L
class CommitChangesViewModel @Inject constructor(
private val tabState: TabState,
private val getCommitDiffEntriesUseCase: GetCommitDiffEntriesUseCase,
tabScope: CoroutineScope,
) {
private val _commitChangesStatus = MutableStateFlow<CommitChangesStatus>(CommitChangesStatus.Loading)
val commitChangesStatus: StateFlow<CommitChangesStatus> = _commitChangesStatus
private val _showSearch = MutableStateFlow(false)
val showSearch: StateFlow<Boolean> = _showSearch
private val _searchFilter = MutableStateFlow(TextFieldValue(""))
val searchFilter: StateFlow<TextFieldValue> = _searchFilter
private val _commitChangesState = MutableStateFlow<CommitChangesState>(CommitChangesState.Loading)
val commitChangesState: StateFlow<CommitChangesState> =
combine(_commitChangesState, _showSearch, _searchFilter) { state, showSearch, filter ->
if (state is CommitChangesState.Loaded) {
if (showSearch && filter.text.isNotBlank()) {
state.copy(changesFiltered = state.changes.filter { it.filePath.lowercaseContains(filter.text) })
} else {
state
}
} else {
state
}
}.stateIn(
tabScope,
SharingStarted.Lazily,
CommitChangesState.Loading
)
fun loadChanges(commit: RevCommit) = tabState.runOperation(
refreshType = RefreshType.NONE,
) { git ->
delayedStateChange(
delayMs = MIN_TIME_IN_MS_TO_SHOW_LOAD,
onDelayTriggered = { _commitChangesStatus.value = CommitChangesStatus.Loading }
) {
val changes = getCommitDiffEntriesUseCase(git, commit)
val state = _commitChangesState.value
_commitChangesStatus.value = CommitChangesStatus.Loaded(commit, changes)
// Check if it's a different commit before resetting everything
if (
state is CommitChangesState.Loading ||
state is CommitChangesState.Loaded && state.commit != commit
) {
delayedStateChange(
delayMs = MIN_TIME_IN_MS_TO_SHOW_LOAD,
onDelayTriggered = { _commitChangesState.value = CommitChangesState.Loading }
) {
val changes = getCommitDiffEntriesUseCase(git, commit)
_commitChangesState.value = CommitChangesState.Loaded(commit, changes, changes)
}
_showSearch.value = false
_searchFilter.value = TextFieldValue("")
}
}
fun onSearchFilterToggled(visible: Boolean) {
_showSearch.value = visible
}
fun onSearchFilterChanged(filter: TextFieldValue) {
_searchFilter.value = filter
}
}
sealed class CommitChangesStatus {
object Loading : CommitChangesStatus()
data class Loaded(val commit: RevCommit, val changes: List<DiffEntry>) : CommitChangesStatus()
sealed class CommitChangesState {
object Loading : CommitChangesState()
data class Loaded(val commit: RevCommit, val changes: List<DiffEntry>, val changesFiltered: List<DiffEntry>) :
CommitChangesState()
}

View File

@ -1,9 +1,11 @@
package com.jetpackduba.gitnuro.viewmodels
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.ui.text.input.TextFieldValue
import com.jetpackduba.gitnuro.extensions.delayedStateChange
import com.jetpackduba.gitnuro.extensions.isMerging
import com.jetpackduba.gitnuro.extensions.isReverting
import com.jetpackduba.gitnuro.extensions.lowercaseContains
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.author.LoadAuthorUseCase
@ -18,10 +20,7 @@ import com.jetpackduba.gitnuro.git.workspace.*
import com.jetpackduba.gitnuro.models.AuthorInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
@ -54,8 +53,50 @@ class StatusViewModel @Inject constructor(
private val saveAuthorUseCase: SaveAuthorUseCase,
private val tabScope: CoroutineScope,
) {
private val _stageStatus = MutableStateFlow<StageStatus>(StageStatus.Loaded(listOf(), listOf(), false))
val stageStatus: StateFlow<StageStatus> = _stageStatus
private val _showSearchUnstaged = MutableStateFlow(false)
val showSearchUnstaged: StateFlow<Boolean> = _showSearchUnstaged
private val _showSearchStaged = MutableStateFlow(false)
val showSearchStaged: StateFlow<Boolean> = _showSearchStaged
private val _searchFilterUnstaged = MutableStateFlow(TextFieldValue(""))
val searchFilterUnstaged: StateFlow<TextFieldValue> = _searchFilterUnstaged
private val _searchFilterStaged = MutableStateFlow(TextFieldValue(""))
val searchFilterStaged: StateFlow<TextFieldValue> = _searchFilterStaged
private val _stageState = MutableStateFlow<StageState>(StageState.Loading)
val stageState: StateFlow<StageState> = combine(
_stageState,
_showSearchStaged,
_searchFilterStaged,
_showSearchUnstaged,
_searchFilterUnstaged,
) { state, showSearchStaged, filterStaged, showSearchUnstaged, filterUnstaged ->
if (state is StageState.Loaded) {
val unstaged = if (showSearchUnstaged && filterUnstaged.text.isNotBlank()) {
state.unstaged.filter { it.filePath.lowercaseContains(filterUnstaged.text) }
} else {
state.unstaged
}
val staged = if (showSearchStaged && filterStaged.text.isNotBlank()) {
state.staged.filter { it.filePath.lowercaseContains(filterStaged.text) }
} else {
state.staged
}
state.copy(stagedFiltered = staged, unstagedFiltered = unstaged)
} else {
state
}
}.stateIn(
tabScope,
SharingStarted.Lazily,
StageState.Loading
)
var savedCommitMessage = CommitMessage("", MessageType.NORMAL)
@ -145,7 +186,7 @@ class StatusViewModel @Inject constructor(
}
private suspend fun loadStatus(git: Git) {
val previousStatus = _stageStatus.value
val previousStatus = _stageState.value
val requiredMessageType = if (git.repository.repositoryState == RepositoryState.MERGING) {
MessageType.MERGE
@ -166,10 +207,10 @@ class StatusViewModel @Inject constructor(
delayedStateChange(
delayMs = MIN_TIME_IN_MS_TO_SHOW_LOAD,
onDelayTriggered = {
if (previousStatus is StageStatus.Loaded) {
_stageStatus.value = previousStatus.copy(isPartiallyReloading = true)
if (previousStatus is StageState.Loaded) {
_stageState.value = previousStatus.copy(isPartiallyReloading = true)
} else {
_stageStatus.value = StageStatus.Loading
_stageState.value = StageState.Loading
}
}
) {
@ -177,10 +218,15 @@ class StatusViewModel @Inject constructor(
val staged = getStagedUseCase(status).sortedBy { it.filePath }
val unstaged = getUnstagedUseCase(status).sortedBy { it.filePath }
_stageStatus.value = StageStatus.Loaded(staged, unstaged, isPartiallyReloading = false)
_stageState.value = StageState.Loaded(
staged = staged,
stagedFiltered = staged,
unstaged = unstaged,
unstagedFiltered = unstaged, isPartiallyReloading = false
)
}
} catch (ex: Exception) {
_stageStatus.value = previousStatus
_stageState.value = previousStatus
throw ex
}
}
@ -330,15 +376,33 @@ class StatusViewModel @Inject constructor(
fun onAcceptCommitterData(newAuthorInfo: AuthorInfo, persist: Boolean) {
this._committerDataRequestState.value = CommitterDataRequestState.Accepted(newAuthorInfo, persist)
}
fun onSearchFilterToggledStaged(visible: Boolean) {
_showSearchStaged.value = visible
}
fun onSearchFilterChangedStaged(filter: TextFieldValue) {
_searchFilterStaged.value = filter
}
fun onSearchFilterToggledUnstaged(visible: Boolean) {
_showSearchUnstaged.value = visible
}
fun onSearchFilterChangedUnstaged(filter: TextFieldValue) {
_searchFilterUnstaged.value = filter
}
}
sealed class StageStatus {
object Loading : StageStatus()
sealed class StageState {
object Loading : StageState()
data class Loaded(
val staged: List<StatusEntry>,
val stagedFiltered: List<StatusEntry>,
val unstaged: List<StatusEntry>,
val unstagedFiltered: List<StatusEntry>,
val isPartiallyReloading: Boolean
) : StageStatus()
) : StageState()
}
data class CommitMessage(val message: String, val messageType: MessageType)

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M18 7l-1.41-1.41-6.34 6.34 1.41 1.41L18 7zm4.24-1.41L11.66 16.17 7.48 12l-1.41 1.41L11.66 19l12-12-1.42-1.41zM.41 13.41L6 19l1.41-1.41L1.83 12 .41 13.41z"/></svg>

After

Width:  |  Height:  |  Size: 310 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0zm0 0h24v24H0V0z" fill="none"/><path d="M1.79 12l5.58 5.59L5.96 19 .37 13.41 1.79 12zm.45-7.78L12.9 14.89l-1.28 1.28L7.44 12l-1.41 1.41L11.62 19l2.69-2.69 4.89 4.89 1.41-1.41L3.65 2.81 2.24 4.22zm14.9 9.27L23.62 7 22.2 5.59l-6.48 6.48 1.42 1.42zM17.96 7l-1.41-1.41-3.65 3.66 1.41 1.41L17.96 7z"/></svg>

After

Width:  |  Height:  |  Size: 425 B