parent
91094a8771
commit
9dfd5073bd
@ -17,6 +17,7 @@ object AppIcons {
|
|||||||
const val COPY = "copy.svg"
|
const val COPY = "copy.svg"
|
||||||
const val CUT = "cut.svg"
|
const val CUT = "cut.svg"
|
||||||
const val DELETE = "delete.svg"
|
const val DELETE = "delete.svg"
|
||||||
|
const val DONE = "done.svg"
|
||||||
const val DOWNLOAD = "download.svg"
|
const val DOWNLOAD = "download.svg"
|
||||||
const val DROPDOWN = "dropdown.svg"
|
const val DROPDOWN = "dropdown.svg"
|
||||||
const val ERROR = "error.svg"
|
const val ERROR = "error.svg"
|
||||||
@ -37,6 +38,7 @@ object AppIcons {
|
|||||||
const val PERSON = "person.svg"
|
const val PERSON = "person.svg"
|
||||||
const val REFRESH = "refresh.svg"
|
const val REFRESH = "refresh.svg"
|
||||||
const val REMOVE = "remove.svg"
|
const val REMOVE = "remove.svg"
|
||||||
|
const val REMOVE_DONE = "remove_done.svg"
|
||||||
const val REVERT = "revert.svg"
|
const val REVERT = "revert.svg"
|
||||||
const val SEARCH = "search.svg"
|
const val SEARCH = "search.svg"
|
||||||
const val SETTINGS = "settings.svg"
|
const val SETTINGS = "settings.svg"
|
||||||
|
@ -11,11 +11,11 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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.components.*
|
||||||
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
|
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
|
||||||
import com.jetpackduba.gitnuro.ui.context_menu.commitedChangesEntriesContextMenuItems
|
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 com.jetpackduba.gitnuro.viewmodels.CommitChangesViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -48,21 +48,36 @@ fun CommitChanges(
|
|||||||
commitChangesViewModel.loadChanges(selectedItem.revCommit)
|
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)
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant)
|
||||||
}
|
}
|
||||||
|
|
||||||
is CommitChangesStatus.Loaded -> {
|
is CommitChangesState.Loaded -> {
|
||||||
CommitChangesView(
|
CommitChangesView(
|
||||||
diffSelected = diffSelected,
|
diffSelected = diffSelected,
|
||||||
commit = commitChangesStatus.commit,
|
commit = commitChangesStatus.commit,
|
||||||
changes = commitChangesStatus.changes,
|
changes = commitChangesStatus.changesFiltered,
|
||||||
onDiffSelected = onDiffSelected,
|
onDiffSelected = onDiffSelected,
|
||||||
onBlame = onBlame,
|
onBlame = onBlame,
|
||||||
onHistory = onHistory,
|
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?,
|
diffSelected: DiffEntryType?,
|
||||||
onBlame: (String) -> Unit,
|
onBlame: (String) -> Unit,
|
||||||
onHistory: (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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 8.dp, bottom = 8.dp)
|
.padding(end = 8.dp, bottom = 8.dp)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
val scroll = rememberScrollState(0)
|
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() }
|
val searchFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@ -116,43 +139,40 @@ fun CommitChangesView(
|
|||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
showSearch = !showSearch
|
onSearchFilterToggled(!showSearch)
|
||||||
}
|
|
||||||
|
if (!showSearch)
|
||||||
|
requestFocus = true
|
||||||
|
},
|
||||||
|
modifier = Modifier.handOnHover(),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(AppIcons.SEARCH),
|
painter = painterResource(AppIcons.SEARCH),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
tint = MaterialTheme.colors.onBackground,
|
tint = MaterialTheme.colors.onBackground,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showSearch) {
|
if (showSearch) {
|
||||||
AdjustableOutlinedTextField(
|
SearchTextField(
|
||||||
value = searchFilter,
|
searchFilter = searchFilter,
|
||||||
onValueChange = {
|
onSearchFilterChanged = onSearchFilterChanged,
|
||||||
searchFilter = it
|
searchFocusRequester = searchFocusRequester,
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
.focusable()
|
|
||||||
.focusRequester(searchFocusRequester)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(showSearch) {
|
LaunchedEffect(showSearch, requestFocus) {
|
||||||
if (showSearch) {
|
if (showSearch && requestFocus) {
|
||||||
searchFocusRequester.requestFocus()
|
searchFocusRequester.requestFocus()
|
||||||
|
requestFocus = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CommitLogChanges(
|
CommitLogChanges(
|
||||||
diffSelected = diffSelected,
|
diffSelected = diffSelected,
|
||||||
diffEntries = if (showSearch && searchFilter.isNotBlank()) changes.filter {
|
diffEntries = changes,
|
||||||
it.filePath.lowercaseContains(
|
|
||||||
searchFilter
|
|
||||||
)
|
|
||||||
} else changes,
|
|
||||||
onDiffSelected = onDiffSelected,
|
onDiffSelected = onDiffSelected,
|
||||||
onBlame = onBlame,
|
onBlame = onBlame,
|
||||||
onHistory = onHistory,
|
onHistory = onHistory,
|
||||||
|
@ -8,6 +8,7 @@ import androidx.compose.animation.fadeIn
|
|||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.hoverable
|
import androidx.compose.foundation.hoverable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||||
@ -23,14 +24,19 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.Color
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
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.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Density
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.jetpackduba.gitnuro.AppIcons
|
||||||
import com.jetpackduba.gitnuro.extensions.*
|
import com.jetpackduba.gitnuro.extensions.*
|
||||||
import com.jetpackduba.gitnuro.git.DiffEntryType
|
import com.jetpackduba.gitnuro.git.DiffEntryType
|
||||||
import com.jetpackduba.gitnuro.git.workspace.StatusEntry
|
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.KeybindingOption
|
||||||
import com.jetpackduba.gitnuro.keybindings.matchesBinding
|
import com.jetpackduba.gitnuro.keybindings.matchesBinding
|
||||||
import com.jetpackduba.gitnuro.theme.*
|
import com.jetpackduba.gitnuro.theme.*
|
||||||
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
|
import com.jetpackduba.gitnuro.ui.components.*
|
||||||
import com.jetpackduba.gitnuro.ui.components.SecondaryButton
|
|
||||||
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
|
|
||||||
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
|
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
|
||||||
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement
|
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement
|
||||||
import com.jetpackduba.gitnuro.ui.context_menu.EntryType
|
import com.jetpackduba.gitnuro.ui.context_menu.EntryType
|
||||||
import com.jetpackduba.gitnuro.ui.context_menu.statusEntriesContextMenuItems
|
import com.jetpackduba.gitnuro.ui.context_menu.statusEntriesContextMenuItems
|
||||||
import com.jetpackduba.gitnuro.ui.dialogs.CommitAuthorDialog
|
import com.jetpackduba.gitnuro.ui.dialogs.CommitAuthorDialog
|
||||||
import com.jetpackduba.gitnuro.viewmodels.CommitterDataRequestState
|
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 com.jetpackduba.gitnuro.viewmodels.StatusViewModel
|
||||||
import org.eclipse.jgit.lib.RepositoryState
|
import org.eclipse.jgit.lib.RepositoryState
|
||||||
|
|
||||||
@ -61,7 +65,7 @@ fun UncommitedChanges(
|
|||||||
onBlameFile: (String) -> Unit,
|
onBlameFile: (String) -> Unit,
|
||||||
onHistoryFile: (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) }
|
var commitMessage by remember(statusViewModel) { mutableStateOf(statusViewModel.savedCommitMessage.message) }
|
||||||
val stagedListState by statusViewModel.stagedLazyListState.collectAsState()
|
val stagedListState by statusViewModel.stagedLazyListState.collectAsState()
|
||||||
val unstagedListState by statusViewModel.unstagedLazyListState.collectAsState()
|
val unstagedListState by statusViewModel.unstagedLazyListState.collectAsState()
|
||||||
@ -69,14 +73,18 @@ fun UncommitedChanges(
|
|||||||
val committerDataRequestState = statusViewModel.committerDataRequestState.collectAsState()
|
val committerDataRequestState = statusViewModel.committerDataRequestState.collectAsState()
|
||||||
val committerDataRequestStateValue = committerDataRequestState.value
|
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 staged: List<StatusEntry>
|
||||||
val unstaged: List<StatusEntry>
|
val unstaged: List<StatusEntry>
|
||||||
val isLoading: Boolean
|
val isLoading: Boolean
|
||||||
|
|
||||||
if (stageStatus is StageStatus.Loaded) {
|
if (stageStatus is StageState.Loaded) {
|
||||||
staged = stageStatus.staged
|
staged = stageStatus.stagedFiltered
|
||||||
unstaged = stageStatus.unstaged
|
unstaged = stageStatus.unstagedFiltered
|
||||||
isLoading = stageStatus.isPartiallyReloading
|
isLoading = stageStatus.isPartiallyReloading
|
||||||
} else {
|
} else {
|
||||||
staged = listOf()
|
staged = listOf()
|
||||||
@ -133,12 +141,21 @@ fun UncommitedChanges(
|
|||||||
title = "Staged",
|
title = "Staged",
|
||||||
allActionTitle = "Unstage all",
|
allActionTitle = "Unstage all",
|
||||||
actionTitle = "Unstage",
|
actionTitle = "Unstage",
|
||||||
|
actionIcon = AppIcons.REMOVE_DONE,
|
||||||
selectedEntryType = if (selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null,
|
selectedEntryType = if (selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null,
|
||||||
actionColor = MaterialTheme.colors.error,
|
actionColor = MaterialTheme.colors.error,
|
||||||
actionTextColor = MaterialTheme.colors.onError,
|
actionTextColor = MaterialTheme.colors.onError,
|
||||||
statusEntries = staged,
|
statusEntries = staged,
|
||||||
lazyListState = stagedListState,
|
lazyListState = stagedListState,
|
||||||
onDiffEntrySelected = onStagedDiffEntrySelected,
|
onDiffEntrySelected = onStagedDiffEntrySelected,
|
||||||
|
showSearch = showSearchStaged,
|
||||||
|
searchFilter = searchFilterStaged,
|
||||||
|
onSearchFilterToggled = {
|
||||||
|
statusViewModel.onSearchFilterToggledStaged(it)
|
||||||
|
},
|
||||||
|
onSearchFilterChanged = {
|
||||||
|
statusViewModel.onSearchFilterChangedStaged(it)
|
||||||
|
},
|
||||||
onDiffEntryOptionSelected = {
|
onDiffEntryOptionSelected = {
|
||||||
statusViewModel.unstage(it)
|
statusViewModel.unstage(it)
|
||||||
},
|
},
|
||||||
@ -163,12 +180,21 @@ fun UncommitedChanges(
|
|||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
title = "Unstaged",
|
title = "Unstaged",
|
||||||
actionTitle = "Stage",
|
actionTitle = "Stage",
|
||||||
|
actionIcon = AppIcons.DONE,
|
||||||
selectedEntryType = if (selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null,
|
selectedEntryType = if (selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null,
|
||||||
actionColor = MaterialTheme.colors.primary,
|
actionColor = MaterialTheme.colors.primary,
|
||||||
actionTextColor = MaterialTheme.colors.onPrimary,
|
actionTextColor = MaterialTheme.colors.onPrimary,
|
||||||
statusEntries = unstaged,
|
statusEntries = unstaged,
|
||||||
lazyListState = unstagedListState,
|
lazyListState = unstagedListState,
|
||||||
onDiffEntrySelected = onUnstagedDiffEntrySelected,
|
onDiffEntrySelected = onUnstagedDiffEntrySelected,
|
||||||
|
showSearch = showSearchUnstaged,
|
||||||
|
searchFilter = searchFilterUnstaged,
|
||||||
|
onSearchFilterToggled = {
|
||||||
|
statusViewModel.onSearchFilterToggledUnstaged(it)
|
||||||
|
},
|
||||||
|
onSearchFilterChanged = {
|
||||||
|
statusViewModel.onSearchFilterChangedUnstaged(it)
|
||||||
|
},
|
||||||
onDiffEntryOptionSelected = {
|
onDiffEntryOptionSelected = {
|
||||||
statusViewModel.stage(it)
|
statusViewModel.stage(it)
|
||||||
},
|
},
|
||||||
@ -558,6 +584,11 @@ private fun EntriesList(
|
|||||||
actionTitle: String,
|
actionTitle: String,
|
||||||
actionColor: Color,
|
actionColor: Color,
|
||||||
actionTextColor: Color,
|
actionTextColor: Color,
|
||||||
|
actionIcon: String,
|
||||||
|
showSearch: Boolean,
|
||||||
|
searchFilter: TextFieldValue,
|
||||||
|
onSearchFilterToggled: (Boolean) -> Unit,
|
||||||
|
onSearchFilterChanged: (TextFieldValue) -> Unit,
|
||||||
statusEntries: List<StatusEntry>,
|
statusEntries: List<StatusEntry>,
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
onDiffEntrySelected: (StatusEntry) -> Unit,
|
onDiffEntrySelected: (StatusEntry) -> Unit,
|
||||||
@ -567,19 +598,30 @@ private fun EntriesList(
|
|||||||
allActionTitle: String,
|
allActionTitle: String,
|
||||||
selectedEntryType: DiffEntryType?,
|
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(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
Box(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(34.dp)
|
.height(34.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(color = MaterialTheme.colors.tertiarySurface)
|
.background(color = MaterialTheme.colors.tertiarySurface)
|
||||||
|
.hoverable(headerHoverInteraction),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
.padding(start = 16.dp, end = 8.dp)
|
||||||
.fillMaxWidth(),
|
.weight(1f),
|
||||||
text = title,
|
text = title,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
textAlign = TextAlign.Left,
|
textAlign = TextAlign.Left,
|
||||||
@ -588,15 +630,49 @@ private fun EntriesList(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
SecondaryButton(
|
IconButton(
|
||||||
modifier = Modifier.align(Alignment.CenterEnd),
|
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,
|
text = allActionTitle,
|
||||||
|
icon = actionIcon,
|
||||||
|
isParentHovered = isHeaderHovered,
|
||||||
backgroundButton = actionColor,
|
backgroundButton = actionColor,
|
||||||
textColor = actionTextColor,
|
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(
|
ScrollableLazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
@ -21,6 +21,7 @@ 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.text.TextStyle
|
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.input.VisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -125,4 +126,104 @@ 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -146,7 +146,8 @@ fun RepositoriesTabPanel(
|
|||||||
fun Tab(
|
fun Tab(
|
||||||
title: MutableState<String>,
|
title: MutableState<String>,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
onClick: () -> Unit, onCloseTab: () -> Unit
|
onClick: () -> Unit,
|
||||||
|
onCloseTab: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val backgroundColor = if (isSelected)
|
val backgroundColor = if (isSelected)
|
||||||
MaterialTheme.colors.surface
|
MaterialTheme.colors.surface
|
||||||
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,29 @@
|
|||||||
package com.jetpackduba.gitnuro.ui.components
|
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.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.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
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.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.jetpackduba.gitnuro.extensions.handMouseClickable
|
import com.jetpackduba.gitnuro.extensions.handMouseClickable
|
||||||
|
import com.jetpackduba.gitnuro.theme.tertiarySurface
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SecondaryButton(
|
fun SecondaryButton(
|
||||||
@ -24,8 +36,7 @@ fun SecondaryButton(
|
|||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(horizontal = 16.dp)
|
.clip(RoundedCornerShape(4.dp))
|
||||||
.clip(RoundedCornerShape(5.dp))
|
|
||||||
.background(backgroundButton)
|
.background(backgroundButton)
|
||||||
.handMouseClickable { onClick() },
|
.handMouseClickable { onClick() },
|
||||||
) {
|
) {
|
||||||
@ -37,4 +48,73 @@ fun SecondaryButton(
|
|||||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 16.dp)
|
modifier = Modifier.padding(vertical = 4.dp, horizontal = 16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -626,13 +626,15 @@ fun HunkHeader(
|
|||||||
text = "Discard hunk",
|
text = "Discard hunk",
|
||||||
backgroundButton = MaterialTheme.colors.error,
|
backgroundButton = MaterialTheme.colors.error,
|
||||||
textColor = MaterialTheme.colors.onError,
|
textColor = MaterialTheme.colors.onError,
|
||||||
onClick = onResetHunk
|
onClick = onResetHunk,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
text = buttonText,
|
text = buttonText,
|
||||||
backgroundButton = color,
|
backgroundButton = color,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (diffEntryType is DiffEntryType.StagedDiff) {
|
if (diffEntryType is DiffEntryType.StagedDiff) {
|
||||||
onUnstageHunk()
|
onUnstageHunk()
|
||||||
@ -787,6 +789,7 @@ fun UncommitedDiffFileHeaderButtons(
|
|||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
text = buttonText,
|
text = buttonText,
|
||||||
backgroundButton = color,
|
backgroundButton = color,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (diffEntryType is DiffEntryType.StagedDiff) {
|
if (diffEntryType is DiffEntryType.StagedDiff) {
|
||||||
onUnstageFile(diffEntryType.statusEntry)
|
onUnstageFile(diffEntryType.statusEntry)
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
package com.jetpackduba.gitnuro.viewmodels
|
package com.jetpackduba.gitnuro.viewmodels
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import com.jetpackduba.gitnuro.extensions.delayedStateChange
|
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.RefreshType
|
||||||
import com.jetpackduba.gitnuro.git.TabState
|
import com.jetpackduba.gitnuro.git.TabState
|
||||||
import com.jetpackduba.gitnuro.git.diff.GetCommitDiffEntriesUseCase
|
import com.jetpackduba.gitnuro.git.diff.GetCommitDiffEntriesUseCase
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.*
|
||||||
import org.eclipse.jgit.diff.DiffEntry
|
import org.eclipse.jgit.diff.DiffEntry
|
||||||
import org.eclipse.jgit.revwalk.RevCommit
|
import org.eclipse.jgit.revwalk.RevCommit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -15,26 +18,69 @@ private const val MIN_TIME_IN_MS_TO_SHOW_LOAD = 300L
|
|||||||
class CommitChangesViewModel @Inject constructor(
|
class CommitChangesViewModel @Inject constructor(
|
||||||
private val tabState: TabState,
|
private val tabState: TabState,
|
||||||
private val getCommitDiffEntriesUseCase: GetCommitDiffEntriesUseCase,
|
private val getCommitDiffEntriesUseCase: GetCommitDiffEntriesUseCase,
|
||||||
|
tabScope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
private val _commitChangesStatus = MutableStateFlow<CommitChangesStatus>(CommitChangesStatus.Loading)
|
private val _showSearch = MutableStateFlow(false)
|
||||||
val commitChangesStatus: StateFlow<CommitChangesStatus> = _commitChangesStatus
|
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(
|
fun loadChanges(commit: RevCommit) = tabState.runOperation(
|
||||||
refreshType = RefreshType.NONE,
|
refreshType = RefreshType.NONE,
|
||||||
) { git ->
|
) { git ->
|
||||||
delayedStateChange(
|
val state = _commitChangesState.value
|
||||||
delayMs = MIN_TIME_IN_MS_TO_SHOW_LOAD,
|
|
||||||
onDelayTriggered = { _commitChangesStatus.value = CommitChangesStatus.Loading }
|
|
||||||
) {
|
|
||||||
val changes = getCommitDiffEntriesUseCase(git, commit)
|
|
||||||
|
|
||||||
_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 {
|
sealed class CommitChangesState {
|
||||||
object Loading : CommitChangesStatus()
|
object Loading : CommitChangesState()
|
||||||
data class Loaded(val commit: RevCommit, val changes: List<DiffEntry>) : CommitChangesStatus()
|
data class Loaded(val commit: RevCommit, val changes: List<DiffEntry>, val changesFiltered: List<DiffEntry>) :
|
||||||
|
CommitChangesState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package com.jetpackduba.gitnuro.viewmodels
|
package com.jetpackduba.gitnuro.viewmodels
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import com.jetpackduba.gitnuro.extensions.delayedStateChange
|
import com.jetpackduba.gitnuro.extensions.delayedStateChange
|
||||||
import com.jetpackduba.gitnuro.extensions.isMerging
|
import com.jetpackduba.gitnuro.extensions.isMerging
|
||||||
import com.jetpackduba.gitnuro.extensions.isReverting
|
import com.jetpackduba.gitnuro.extensions.isReverting
|
||||||
|
import com.jetpackduba.gitnuro.extensions.lowercaseContains
|
||||||
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.author.LoadAuthorUseCase
|
import com.jetpackduba.gitnuro.git.author.LoadAuthorUseCase
|
||||||
@ -18,10 +20,7 @@ import com.jetpackduba.gitnuro.git.workspace.*
|
|||||||
import com.jetpackduba.gitnuro.models.AuthorInfo
|
import com.jetpackduba.gitnuro.models.AuthorInfo
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
@ -54,8 +53,50 @@ class StatusViewModel @Inject constructor(
|
|||||||
private val saveAuthorUseCase: SaveAuthorUseCase,
|
private val saveAuthorUseCase: SaveAuthorUseCase,
|
||||||
private val tabScope: CoroutineScope,
|
private val tabScope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
private val _stageStatus = MutableStateFlow<StageStatus>(StageStatus.Loaded(listOf(), listOf(), false))
|
private val _showSearchUnstaged = MutableStateFlow(false)
|
||||||
val stageStatus: StateFlow<StageStatus> = _stageStatus
|
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)
|
var savedCommitMessage = CommitMessage("", MessageType.NORMAL)
|
||||||
|
|
||||||
@ -145,7 +186,7 @@ class StatusViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadStatus(git: Git) {
|
private suspend fun loadStatus(git: Git) {
|
||||||
val previousStatus = _stageStatus.value
|
val previousStatus = _stageState.value
|
||||||
|
|
||||||
val requiredMessageType = if (git.repository.repositoryState == RepositoryState.MERGING) {
|
val requiredMessageType = if (git.repository.repositoryState == RepositoryState.MERGING) {
|
||||||
MessageType.MERGE
|
MessageType.MERGE
|
||||||
@ -166,10 +207,10 @@ class StatusViewModel @Inject constructor(
|
|||||||
delayedStateChange(
|
delayedStateChange(
|
||||||
delayMs = MIN_TIME_IN_MS_TO_SHOW_LOAD,
|
delayMs = MIN_TIME_IN_MS_TO_SHOW_LOAD,
|
||||||
onDelayTriggered = {
|
onDelayTriggered = {
|
||||||
if (previousStatus is StageStatus.Loaded) {
|
if (previousStatus is StageState.Loaded) {
|
||||||
_stageStatus.value = previousStatus.copy(isPartiallyReloading = true)
|
_stageState.value = previousStatus.copy(isPartiallyReloading = true)
|
||||||
} else {
|
} 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 staged = getStagedUseCase(status).sortedBy { it.filePath }
|
||||||
val unstaged = getUnstagedUseCase(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) {
|
} catch (ex: Exception) {
|
||||||
_stageStatus.value = previousStatus
|
_stageState.value = previousStatus
|
||||||
throw ex
|
throw ex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -330,15 +376,33 @@ class StatusViewModel @Inject constructor(
|
|||||||
fun onAcceptCommitterData(newAuthorInfo: AuthorInfo, persist: Boolean) {
|
fun onAcceptCommitterData(newAuthorInfo: AuthorInfo, persist: Boolean) {
|
||||||
this._committerDataRequestState.value = CommitterDataRequestState.Accepted(newAuthorInfo, persist)
|
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 {
|
sealed class StageState {
|
||||||
object Loading : StageStatus()
|
object Loading : StageState()
|
||||||
data class Loaded(
|
data class Loaded(
|
||||||
val staged: List<StatusEntry>,
|
val staged: List<StatusEntry>,
|
||||||
|
val stagedFiltered: List<StatusEntry>,
|
||||||
val unstaged: List<StatusEntry>,
|
val unstaged: List<StatusEntry>,
|
||||||
|
val unstagedFiltered: List<StatusEntry>,
|
||||||
val isPartiallyReloading: Boolean
|
val isPartiallyReloading: Boolean
|
||||||
) : StageStatus()
|
) : StageState()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CommitMessage(val message: String, val messageType: MessageType)
|
data class CommitMessage(val message: String, val messageType: MessageType)
|
||||||
|
1
src/main/resources/done.svg
Normal file
1
src/main/resources/done.svg
Normal 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 |
1
src/main/resources/remove_done.svg
Normal file
1
src/main/resources/remove_done.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user