Implemented graph search
Fixed bug where deleting text input value too fast (keeping pressed backspace) would not behave properly
This commit is contained in:
parent
b83133f4fe
commit
054778bdcc
@ -57,7 +57,7 @@ fun UncommitedChanges(
|
|||||||
onUnstagedDiffEntrySelected: (DiffEntry) -> Unit,
|
onUnstagedDiffEntrySelected: (DiffEntry) -> Unit,
|
||||||
) {
|
) {
|
||||||
val stageStatusState = statusViewModel.stageStatus.collectAsState()
|
val stageStatusState = statusViewModel.stageStatus.collectAsState()
|
||||||
val commitMessage by statusViewModel.commitMessage.collectAsState()
|
var commitMessage by remember { mutableStateOf(statusViewModel.savedCommitMessage) }
|
||||||
|
|
||||||
val stageStatus = stageStatusState.value
|
val stageStatus = stageStatusState.value
|
||||||
val staged: List<StatusEntry>
|
val staged: List<StatusEntry>
|
||||||
@ -86,7 +86,7 @@ fun UncommitedChanges(
|
|||||||
val doCommit = { amend: Boolean ->
|
val doCommit = { amend: Boolean ->
|
||||||
statusViewModel.commit(commitMessage, amend)
|
statusViewModel.commit(commitMessage, amend)
|
||||||
onStagedDiffEntrySelected(null)
|
onStagedDiffEntrySelected(null)
|
||||||
statusViewModel.newCommitMessage = ""
|
statusViewModel.savedCommitMessage = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
val canCommit = commitMessage.isNotEmpty() && staged.isNotEmpty()
|
val canCommit = commitMessage.isNotEmpty() && staged.isNotEmpty()
|
||||||
@ -186,7 +186,10 @@ fun UncommitedChanges(
|
|||||||
false
|
false
|
||||||
},
|
},
|
||||||
value = commitMessage,
|
value = commitMessage,
|
||||||
onValueChange = { statusViewModel.newCommitMessage = it },
|
onValueChange = {
|
||||||
|
commitMessage = it
|
||||||
|
statusViewModel.savedCommitMessage = it
|
||||||
|
},
|
||||||
label = { Text("Write your commit message here", fontSize = 14.sp) },
|
label = { Text("Write your commit message here", fontSize = 14.sp) },
|
||||||
colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colors.background),
|
colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colors.background),
|
||||||
textStyle = TextStyle.Default.copy(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
|
textStyle = TextStyle.Default.copy(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
|
||||||
|
@ -13,25 +13,25 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material.icons.filled.Edit
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
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.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.input.key.*
|
||||||
import androidx.compose.ui.input.pointer.PointerIconDefaults
|
import androidx.compose.ui.input.pointer.PointerIconDefaults
|
||||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
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
|
||||||
@ -49,8 +49,11 @@ import app.ui.context_menu.branchContextMenuItems
|
|||||||
import app.ui.context_menu.logContextMenu
|
import app.ui.context_menu.logContextMenu
|
||||||
import app.ui.context_menu.tagContextMenuItems
|
import app.ui.context_menu.tagContextMenuItems
|
||||||
import app.ui.dialogs.*
|
import app.ui.dialogs.*
|
||||||
|
import app.viewmodels.LogSearch
|
||||||
import app.viewmodels.LogStatus
|
import app.viewmodels.LogStatus
|
||||||
import app.viewmodels.LogViewModel
|
import app.viewmodels.LogViewModel
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.eclipse.jgit.lib.Ref
|
import org.eclipse.jgit.lib.Ref
|
||||||
import org.eclipse.jgit.lib.RepositoryState
|
import org.eclipse.jgit.lib.RepositoryState
|
||||||
import org.eclipse.jgit.revwalk.RevCommit
|
import org.eclipse.jgit.revwalk.RevCommit
|
||||||
@ -66,12 +69,11 @@ private val colors = listOf(
|
|||||||
|
|
||||||
private const val CANVAS_MIN_WIDTH = 100
|
private const val CANVAS_MIN_WIDTH = 100
|
||||||
private const val MIN_GRAPH_LINES = 2
|
private const val MIN_GRAPH_LINES = 2
|
||||||
private const val PADDING_BETWEEN_DIVIDER_AND_MESSAGE = 8
|
private const val DIVIDER_WIDTH = 8
|
||||||
|
|
||||||
// TODO Min size for message column
|
// TODO Min size for message column
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalFoundationApi::class,
|
ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class
|
||||||
ExperimentalComposeUiApi::class
|
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun Log(
|
fun Log(
|
||||||
@ -79,6 +81,7 @@ fun Log(
|
|||||||
selectedItem: SelectedItem,
|
selectedItem: SelectedItem,
|
||||||
repositoryState: RepositoryState,
|
repositoryState: RepositoryState,
|
||||||
) {
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
val logStatusState = logViewModel.logStatus.collectAsState()
|
val logStatusState = logViewModel.logStatus.collectAsState()
|
||||||
val logStatus = logStatusState.value
|
val logStatus = logStatusState.value
|
||||||
val showLogDialog = remember { mutableStateOf<LogDialog>(LogDialog.None) }
|
val showLogDialog = remember { mutableStateOf<LogDialog>(LogDialog.None) }
|
||||||
@ -93,7 +96,8 @@ fun Log(
|
|||||||
val hasUncommitedChanges = logStatus.hasUncommitedChanges
|
val hasUncommitedChanges = logStatus.hasUncommitedChanges
|
||||||
val commitList = logStatus.plotCommitList
|
val commitList = logStatus.plotCommitList
|
||||||
val verticalScrollState = rememberLazyListState()
|
val verticalScrollState = rememberLazyListState()
|
||||||
|
val searchFilter = logViewModel.logSearchFilterResults.collectAsState()
|
||||||
|
val searchFilterValue = searchFilter.value
|
||||||
// With this method, whenever the scroll changes, the log is recomposed and the graph list is updated with
|
// With this method, whenever the scroll changes, the log is recomposed and the graph list is updated with
|
||||||
// the proper scroll position
|
// the proper scroll position
|
||||||
verticalScrollState.observeScrollChanges()
|
verticalScrollState.observeScrollChanges()
|
||||||
@ -101,12 +105,13 @@ fun Log(
|
|||||||
LaunchedEffect(selectedCommit) {
|
LaunchedEffect(selectedCommit) {
|
||||||
// Scroll to commit if a Ref is selected
|
// Scroll to commit if a Ref is selected
|
||||||
if (selectedItem is SelectedItem.Ref) {
|
if (selectedItem is SelectedItem.Ref) {
|
||||||
val index = commitList.indexOfFirst { it.name == selectedCommit?.name }
|
scrollToCommit(verticalScrollState, commitList, selectedCommit)
|
||||||
// TODO Show a message informing the user why we aren't scrolling
|
}
|
||||||
// Index can be -1 if the ref points to a commit that is not shown in the graph due to the limited
|
}
|
||||||
// number of displayed commits.
|
|
||||||
if (index >= 0)
|
LaunchedEffect(Unit) {
|
||||||
verticalScrollState.scrollToItem(index)
|
logViewModel.focusCommit.collect { commit ->
|
||||||
|
scrollToCommit(verticalScrollState, commitList, commit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,19 +123,20 @@ fun Log(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.background(MaterialTheme.colors.background).fillMaxSize()
|
||||||
.background(MaterialTheme.colors.background)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
) {
|
||||||
val weightMod = remember { mutableStateOf(0f) }
|
val weightMod = remember { mutableStateOf(0f) }
|
||||||
var graphWidth = (CANVAS_MIN_WIDTH + weightMod.value).dp
|
var graphWidth = (CANVAS_MIN_WIDTH + weightMod.value).dp
|
||||||
|
|
||||||
if (graphWidth.value < CANVAS_MIN_WIDTH)
|
if (graphWidth.value < CANVAS_MIN_WIDTH) graphWidth = CANVAS_MIN_WIDTH.dp
|
||||||
graphWidth = CANVAS_MIN_WIDTH.dp
|
|
||||||
|
|
||||||
|
if (searchFilterValue is LogSearch.SearchResults) {
|
||||||
|
SearchFilter(logViewModel, searchFilterValue)
|
||||||
|
}
|
||||||
GraphHeader(
|
GraphHeader(
|
||||||
graphWidth = graphWidth,
|
graphWidth = graphWidth,
|
||||||
weightMod = weightMod,
|
weightMod = weightMod,
|
||||||
|
onShowSearch = { scope.launch { logViewModel.onSearchValueChanged("") } }
|
||||||
)
|
)
|
||||||
|
|
||||||
val horizontalScrollState = rememberScrollState(0)
|
val horizontalScrollState = rememberScrollState(0)
|
||||||
@ -148,6 +154,7 @@ fun Log(
|
|||||||
MessagesList(
|
MessagesList(
|
||||||
scrollState = verticalScrollState,
|
scrollState = verticalScrollState,
|
||||||
hasUncommitedChanges = hasUncommitedChanges,
|
hasUncommitedChanges = hasUncommitedChanges,
|
||||||
|
searchFilter = if (searchFilterValue is LogSearch.SearchResults) searchFilterValue.commits else null,
|
||||||
selectedCommit = selectedCommit,
|
selectedCommit = selectedCommit,
|
||||||
logStatus = logStatus,
|
logStatus = logStatus,
|
||||||
repositoryState = repositoryState,
|
repositoryState = repositoryState,
|
||||||
@ -157,42 +164,147 @@ fun Log(
|
|||||||
graphWidth = graphWidth,
|
graphWidth = graphWidth,
|
||||||
onShowLogDialog = { dialog ->
|
onShowLogDialog = { dialog ->
|
||||||
showLogDialog.value = dialog
|
showLogDialog.value = dialog
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
DividerLog(
|
DividerLog(
|
||||||
modifier = Modifier
|
modifier = Modifier.draggable(
|
||||||
.draggable(
|
rememberDraggableState {
|
||||||
rememberDraggableState {
|
weightMod.value += it
|
||||||
weightMod.value += it
|
}, Orientation.Horizontal
|
||||||
},
|
),
|
||||||
Orientation.Horizontal
|
|
||||||
),
|
|
||||||
graphWidth = graphWidth,
|
graphWidth = graphWidth,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Scrollbar used to scroll horizontally the graph nodes
|
// Scrollbar used to scroll horizontally the graph nodes
|
||||||
// Added after every component to have the highest priority when clicking
|
// Added after every component to have the highest priority when clicking
|
||||||
HorizontalScrollbar(
|
HorizontalScrollbar(
|
||||||
modifier = Modifier
|
modifier = Modifier.align(Alignment.BottomStart).width(graphWidth)
|
||||||
.align(Alignment.BottomStart)
|
.padding(start = 4.dp, bottom = 4.dp), style = LocalScrollbarStyle.current.copy(
|
||||||
.width(graphWidth)
|
|
||||||
.padding(start = 4.dp, bottom = 4.dp),
|
|
||||||
style = LocalScrollbarStyle.current.copy(
|
|
||||||
unhoverColor = MaterialTheme.colors.scrollbarUnhover,
|
unhoverColor = MaterialTheme.colors.scrollbarUnhover,
|
||||||
hoverColor = MaterialTheme.colors.scrollbarHover,
|
hoverColor = MaterialTheme.colors.scrollbarHover,
|
||||||
),
|
), adapter = rememberScrollbarAdapter(horizontalScrollState)
|
||||||
adapter = rememberScrollbarAdapter(horizontalScrollState)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun scrollToCommit(
|
||||||
|
verticalScrollState: LazyListState,
|
||||||
|
commitList: GraphCommitList,
|
||||||
|
commit: RevCommit?,
|
||||||
|
) {
|
||||||
|
val index = commitList.indexOfFirst { it.name == commit?.name }
|
||||||
|
// TODO Show a message informing the user why we aren't scrolling
|
||||||
|
// Index can be -1 if the ref points to a commit that is not shown in the graph due to the limited
|
||||||
|
// number of displayed commits.
|
||||||
|
if (index >= 0) verticalScrollState.scrollToItem(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchFilter(
|
||||||
|
logViewModel: LogViewModel,
|
||||||
|
searchFilterResults: LogSearch.SearchResults
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var searchFilterText by remember { mutableStateOf(logViewModel.savedSearchFilter) }
|
||||||
|
val textFieldFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
textFieldFocusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(64.dp)
|
||||||
|
.background(MaterialTheme.colors.graphHeaderBackground),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
value = searchFilterText,
|
||||||
|
onValueChange = {
|
||||||
|
searchFilterText = it
|
||||||
|
scope.launch {
|
||||||
|
logViewModel.onSearchValueChanged(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.focusRequester(textFieldFocusRequester)
|
||||||
|
.onPreviewKeyEvent {
|
||||||
|
when {
|
||||||
|
it.key == Key.Enter && it.type == KeyEventType.KeyUp-> {
|
||||||
|
scope.launch {
|
||||||
|
logViewModel.selectNextFilterCommit()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
it.key == Key.Escape && it.type == KeyEventType.KeyUp -> {
|
||||||
|
logViewModel.closeSearch()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text("Search by message, author name or commit ID")
|
||||||
|
},
|
||||||
|
colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colors.background),
|
||||||
|
textStyle = TextStyle.Default.copy(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
|
||||||
|
trailingIcon = {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (searchFilterText.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
"${searchFilterResults.index}/${searchFilterResults.totalCount}",
|
||||||
|
color = MaterialTheme.colors.secondaryTextColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.pointerHoverIcon(PointerIconDefaults.Hand),
|
||||||
|
onClick = {
|
||||||
|
scope.launch { logViewModel.selectPreviousFilterCommit() }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.KeyboardArrowUp, contentDescription = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.pointerHoverIcon(PointerIconDefaults.Hand),
|
||||||
|
onClick = {
|
||||||
|
scope.launch { logViewModel.selectNextFilterCommit() }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.KeyboardArrowDown, contentDescription = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.pointerHoverIcon(PointerIconDefaults.Hand)
|
||||||
|
.padding(end = 4.dp),
|
||||||
|
onClick = { logViewModel.closeSearch() }
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Clear, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessagesList(
|
fun MessagesList(
|
||||||
scrollState: LazyListState,
|
scrollState: LazyListState,
|
||||||
hasUncommitedChanges: Boolean,
|
hasUncommitedChanges: Boolean,
|
||||||
|
searchFilter: List<GraphNode>?,
|
||||||
selectedCommit: RevCommit?,
|
selectedCommit: RevCommit?,
|
||||||
logStatus: LogStatus.Loaded,
|
logStatus: LogStatus.Loaded,
|
||||||
repositoryState: RepositoryState,
|
repositoryState: RepositoryState,
|
||||||
@ -204,37 +316,32 @@ fun MessagesList(
|
|||||||
) {
|
) {
|
||||||
ScrollableLazyColumn(
|
ScrollableLazyColumn(
|
||||||
state = scrollState,
|
state = scrollState,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize(),
|
|
||||||
) {
|
) {
|
||||||
if (hasUncommitedChanges)
|
if (hasUncommitedChanges) item {
|
||||||
item {
|
UncommitedChangesLine(graphWidth = graphWidth,
|
||||||
UncommitedChangesLine(
|
selected = selectedItem == SelectedItem.UncommitedChanges,
|
||||||
graphWidth = graphWidth,
|
statusSummary = logStatus.statusSummary,
|
||||||
selected = selectedItem == SelectedItem.UncommitedChanges,
|
repositoryState = repositoryState,
|
||||||
statusSummary = logStatus.statusSummary,
|
onUncommitedChangesSelected = {
|
||||||
repositoryState = repositoryState,
|
logViewModel.selectUncommitedChanges()
|
||||||
onUncommitedChangesSelected = {
|
})
|
||||||
logViewModel.selectLogLine(SelectedItem.UncommitedChanges)
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
items(items = commitList) { graphNode ->
|
items(items = commitList) { graphNode ->
|
||||||
CommitLine(
|
CommitLine(graphWidth = graphWidth,
|
||||||
graphWidth = graphWidth,
|
|
||||||
logViewModel = logViewModel,
|
logViewModel = logViewModel,
|
||||||
graphNode = graphNode,
|
graphNode = graphNode,
|
||||||
selected = selectedCommit?.name == graphNode.name,
|
selected = selectedCommit?.name == graphNode.name,
|
||||||
currentBranch = logStatus.currentBranch,
|
currentBranch = logStatus.currentBranch,
|
||||||
|
matchesSearchFilter = searchFilter?.contains(graphNode),
|
||||||
showCreateNewBranch = { onShowLogDialog(LogDialog.NewBranch(graphNode)) },
|
showCreateNewBranch = { onShowLogDialog(LogDialog.NewBranch(graphNode)) },
|
||||||
showCreateNewTag = { onShowLogDialog(LogDialog.NewTag(graphNode)) },
|
showCreateNewTag = { onShowLogDialog(LogDialog.NewTag(graphNode)) },
|
||||||
resetBranch = { onShowLogDialog(LogDialog.ResetBranch(graphNode)) },
|
resetBranch = { onShowLogDialog(LogDialog.ResetBranch(graphNode)) },
|
||||||
onMergeBranch = { ref -> onShowLogDialog(LogDialog.MergeBranch(ref)) },
|
onMergeBranch = { ref -> onShowLogDialog(LogDialog.MergeBranch(ref)) },
|
||||||
onRebaseBranch = { ref -> onShowLogDialog(LogDialog.RebaseBranch(ref)) },
|
onRebaseBranch = { ref -> onShowLogDialog(LogDialog.RebaseBranch(ref)) },
|
||||||
onRevCommitSelected = {
|
onRevCommitSelected = {
|
||||||
logViewModel.selectLogLine(SelectedItem.Commit(graphNode))
|
logViewModel.selectLogLine(graphNode)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -245,44 +352,32 @@ fun GraphList(
|
|||||||
stateHorizontal: ScrollState = rememberScrollState(0),
|
stateHorizontal: ScrollState = rememberScrollState(0),
|
||||||
graphWidth: Dp,
|
graphWidth: Dp,
|
||||||
scrollState: LazyListState,
|
scrollState: LazyListState,
|
||||||
hasUncommitedChanges: Boolean
|
hasUncommitedChanges: Boolean,
|
||||||
) {
|
) {
|
||||||
val graphRealWidth = remember(commitList, graphWidth) {
|
val graphRealWidth = remember(commitList, graphWidth) {
|
||||||
val maxLinePosition = if (commitList.isNotEmpty())
|
val maxLinePosition = if (commitList.isNotEmpty()) commitList.maxOf { it.lane.position }
|
||||||
commitList.maxOf { it.lane.position }
|
else MIN_GRAPH_LINES
|
||||||
else
|
|
||||||
MIN_GRAPH_LINES
|
|
||||||
|
|
||||||
((maxLinePosition + 2) * 30f).dp
|
((maxLinePosition + 2) * 30f).dp
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier.width(graphWidth).fillMaxHeight()
|
||||||
.width(graphWidth)
|
|
||||||
.fillMaxHeight()
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().horizontalScroll(stateHorizontal).padding(bottom = 8.dp)
|
||||||
.fillMaxSize()
|
|
||||||
.horizontalScroll(stateHorizontal)
|
|
||||||
.padding(bottom = 8.dp)
|
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = scrollState,
|
state = scrollState, modifier = Modifier.width(graphRealWidth)
|
||||||
modifier = Modifier
|
|
||||||
.width(graphRealWidth)
|
|
||||||
) {
|
) {
|
||||||
if (hasUncommitedChanges) {
|
if (hasUncommitedChanges) {
|
||||||
item {
|
item {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.height(40.dp).fillMaxWidth(),
|
||||||
.height(40.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
) {
|
) {
|
||||||
UncommitedChangesGraphNode(
|
UncommitedChangesGraphNode(
|
||||||
modifier = Modifier
|
modifier = Modifier.width(graphWidth),
|
||||||
.width(graphWidth),
|
|
||||||
hasPreviousCommits = commitList.isNotEmpty(),
|
hasPreviousCommits = commitList.isNotEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -292,13 +387,10 @@ fun GraphList(
|
|||||||
items(items = commitList) { graphNode ->
|
items(items = commitList) { graphNode ->
|
||||||
val nodeColor = colors[graphNode.lane.position % colors.size]
|
val nodeColor = colors[graphNode.lane.position % colors.size]
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.height(40.dp).fillMaxWidth(),
|
||||||
.height(40.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
) {
|
) {
|
||||||
CommitsGraphLine(
|
CommitsGraphLine(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxHeight(),
|
||||||
.fillMaxHeight(),
|
|
||||||
plotCommit = graphNode,
|
plotCommit = graphNode,
|
||||||
nodeColor = nodeColor,
|
nodeColor = nodeColor,
|
||||||
)
|
)
|
||||||
@ -318,53 +410,39 @@ fun LogDialogs(
|
|||||||
) {
|
) {
|
||||||
when (showLogDialog) {
|
when (showLogDialog) {
|
||||||
is LogDialog.NewBranch -> {
|
is LogDialog.NewBranch -> {
|
||||||
NewBranchDialog(
|
NewBranchDialog(onReject = onResetShowLogDialog, onAccept = { branchName ->
|
||||||
onReject = onResetShowLogDialog,
|
logViewModel.createBranchOnCommit(branchName, showLogDialog.graphNode)
|
||||||
onAccept = { branchName ->
|
onResetShowLogDialog()
|
||||||
logViewModel.createBranchOnCommit(branchName, showLogDialog.graphNode)
|
})
|
||||||
onResetShowLogDialog()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
is LogDialog.NewTag -> {
|
is LogDialog.NewTag -> {
|
||||||
NewTagDialog(
|
NewTagDialog(onReject = onResetShowLogDialog, onAccept = { tagName ->
|
||||||
onReject = onResetShowLogDialog,
|
logViewModel.createTagOnCommit(tagName, showLogDialog.graphNode)
|
||||||
onAccept = { tagName ->
|
onResetShowLogDialog()
|
||||||
logViewModel.createTagOnCommit(tagName, showLogDialog.graphNode)
|
})
|
||||||
onResetShowLogDialog()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
is LogDialog.MergeBranch -> {
|
is LogDialog.MergeBranch -> {
|
||||||
if (currentBranch != null)
|
if (currentBranch != null) MergeDialog(currentBranchName = currentBranch.simpleName,
|
||||||
MergeDialog(
|
mergeBranchName = showLogDialog.ref.simpleName,
|
||||||
currentBranchName = currentBranch.simpleName,
|
onReject = onResetShowLogDialog,
|
||||||
mergeBranchName = showLogDialog.ref.simpleName,
|
onAccept = { ff ->
|
||||||
onReject = onResetShowLogDialog,
|
logViewModel.mergeBranch(showLogDialog.ref, ff)
|
||||||
onAccept = { ff ->
|
onResetShowLogDialog()
|
||||||
logViewModel.mergeBranch(showLogDialog.ref, ff)
|
})
|
||||||
onResetShowLogDialog()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
is LogDialog.ResetBranch -> ResetBranchDialog(
|
is LogDialog.ResetBranch -> ResetBranchDialog(onReject = onResetShowLogDialog, onAccept = { resetType ->
|
||||||
onReject = onResetShowLogDialog,
|
logViewModel.resetToCommit(showLogDialog.graphNode, resetType)
|
||||||
onAccept = { resetType ->
|
onResetShowLogDialog()
|
||||||
logViewModel.resetToCommit(showLogDialog.graphNode, resetType)
|
})
|
||||||
onResetShowLogDialog()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
is LogDialog.RebaseBranch -> {
|
is LogDialog.RebaseBranch -> {
|
||||||
if (currentBranch != null) {
|
if (currentBranch != null) {
|
||||||
RebaseDialog(
|
RebaseDialog(currentBranchName = currentBranch.simpleName,
|
||||||
currentBranchName = currentBranch.simpleName,
|
|
||||||
rebaseBranchName = showLogDialog.ref.simpleName,
|
rebaseBranchName = showLogDialog.ref.simpleName,
|
||||||
onReject = onResetShowLogDialog,
|
onReject = onResetShowLogDialog,
|
||||||
onAccept = {
|
onAccept = {
|
||||||
logViewModel.rebaseBranch(showLogDialog.ref)
|
logViewModel.rebaseBranch(showLogDialog.ref)
|
||||||
onResetShowLogDialog()
|
onResetShowLogDialog()
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LogDialog.None -> {
|
LogDialog.None -> {
|
||||||
@ -376,43 +454,51 @@ fun LogDialogs(
|
|||||||
fun GraphHeader(
|
fun GraphHeader(
|
||||||
graphWidth: Dp,
|
graphWidth: Dp,
|
||||||
weightMod: MutableState<Float>,
|
weightMod: MutableState<Float>,
|
||||||
|
onShowSearch: () -> Unit
|
||||||
) {
|
) {
|
||||||
Row(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
|
||||||
.height(32.dp)
|
|
||||||
.background(MaterialTheme.colors.graphHeaderBackground),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colors.graphHeaderBackground),
|
||||||
.width(graphWidth)
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
.padding(start = 8.dp),
|
) {
|
||||||
text = "Graph",
|
Text(
|
||||||
color = MaterialTheme.colors.headerText,
|
modifier = Modifier.width(graphWidth).padding(start = 8.dp),
|
||||||
fontSize = 14.sp,
|
text = "Graph",
|
||||||
maxLines = 1,
|
color = MaterialTheme.colors.headerText,
|
||||||
)
|
fontSize = 14.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
|
||||||
SimpleDividerLog(
|
SimpleDividerLog(
|
||||||
modifier = Modifier
|
modifier = Modifier.draggable(
|
||||||
.draggable(
|
|
||||||
rememberDraggableState {
|
rememberDraggableState {
|
||||||
weightMod.value += it
|
weightMod.value += it
|
||||||
},
|
}, Orientation.Horizontal
|
||||||
Orientation.Horizontal
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(start = 8.dp).weight(1f),
|
||||||
.padding(start = 8.dp)
|
text = "Message",
|
||||||
.width(graphWidth),
|
color = MaterialTheme.colors.headerText,
|
||||||
text = "Message",
|
fontSize = 14.sp,
|
||||||
color = MaterialTheme.colors.headerText,
|
maxLines = 1,
|
||||||
fontSize = 14.sp,
|
)
|
||||||
maxLines = 1,
|
|
||||||
)
|
IconButton(
|
||||||
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
onClick = onShowSearch
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Search,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.primaryTextColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,20 +512,15 @@ fun UncommitedChangesLine(
|
|||||||
) {
|
) {
|
||||||
val textColor = if (selected) {
|
val textColor = if (selected) {
|
||||||
MaterialTheme.colors.primary
|
MaterialTheme.colors.primary
|
||||||
} else
|
} else MaterialTheme.colors.primaryTextColor
|
||||||
MaterialTheme.colors.primaryTextColor
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.height(40.dp).fillMaxWidth().clickable {
|
||||||
.height(40.dp)
|
onUncommitedChangesSelected()
|
||||||
.fillMaxWidth()
|
}.padding(
|
||||||
.clickable {
|
start = graphWidth + DIVIDER_WIDTH.dp,
|
||||||
onUncommitedChangesSelected()
|
end = 4.dp,
|
||||||
}
|
),
|
||||||
.padding(
|
|
||||||
start = graphWidth + PADDING_BETWEEN_DIVIDER_AND_MESSAGE.dp,
|
|
||||||
end = 4.dp,
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
val text = when {
|
val text = when {
|
||||||
@ -499,9 +580,7 @@ fun LogStatusSummary(statusSummary: StatusSummary, modifier: Modifier) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SummaryEntry(
|
fun SummaryEntry(
|
||||||
count: Int,
|
count: Int, icon: ImageVector, color: Color
|
||||||
icon: ImageVector,
|
|
||||||
color: Color
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = 4.dp),
|
modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
@ -514,14 +593,12 @@ fun SummaryEntry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon, tint = color, contentDescription = null, modifier = Modifier.size(14.dp)
|
||||||
tint = color,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(14.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CommitLine(
|
fun CommitLine(
|
||||||
graphWidth: Dp,
|
graphWidth: Dp,
|
||||||
@ -529,6 +606,7 @@ fun CommitLine(
|
|||||||
graphNode: GraphNode,
|
graphNode: GraphNode,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
currentBranch: Ref?,
|
currentBranch: Ref?,
|
||||||
|
matchesSearchFilter: Boolean?,
|
||||||
showCreateNewBranch: () -> Unit,
|
showCreateNewBranch: () -> Unit,
|
||||||
showCreateNewTag: () -> Unit,
|
showCreateNewTag: () -> Unit,
|
||||||
resetBranch: (GraphNode) -> Unit,
|
resetBranch: (GraphNode) -> Unit,
|
||||||
@ -548,25 +626,35 @@ fun CommitLine(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier
|
Box(
|
||||||
.clickable {
|
modifier = Modifier.clickable {
|
||||||
onRevCommitSelected(graphNode)
|
onRevCommitSelected(graphNode)
|
||||||
}
|
}.padding(start = graphWidth)
|
||||||
.padding(start = graphWidth + PADDING_BETWEEN_DIVIDER_AND_MESSAGE.dp)
|
.height(40.dp)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
if (matchesSearchFilter == true) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = DIVIDER_WIDTH.dp)
|
||||||
|
.background(MaterialTheme.colors.secondary)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(40.dp)
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(end = 4.dp),
|
.padding(end = 4.dp),
|
||||||
) {
|
) {
|
||||||
val nodeColor = colors[graphNode.lane.position % colors.size]
|
val nodeColor = colors[graphNode.lane.position % colors.size]
|
||||||
CommitMessage(
|
CommitMessage(
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
commit = graphNode,
|
commit = graphNode,
|
||||||
selected = selected,
|
selected = selected,
|
||||||
refs = graphNode.refs,
|
refs = graphNode.refs,
|
||||||
nodeColor = nodeColor,
|
nodeColor = nodeColor,
|
||||||
|
matchesSearchFilter = matchesSearchFilter,
|
||||||
currentBranch = currentBranch,
|
currentBranch = currentBranch,
|
||||||
onCheckoutRef = { ref -> logViewModel.checkoutRef(ref) },
|
onCheckoutRef = { ref -> logViewModel.checkoutRef(ref) },
|
||||||
onMergeBranch = { ref -> onMergeBranch(ref) },
|
onMergeBranch = { ref -> onMergeBranch(ref) },
|
||||||
@ -583,12 +671,12 @@ fun CommitLine(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CommitMessage(
|
fun CommitMessage(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
commit: RevCommit,
|
commit: RevCommit,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
refs: List<Ref>,
|
refs: List<Ref>,
|
||||||
currentBranch: Ref?,
|
currentBranch: Ref?,
|
||||||
nodeColor: Color,
|
nodeColor: Color,
|
||||||
|
matchesSearchFilter: Boolean?,
|
||||||
onCheckoutRef: (ref: Ref) -> Unit,
|
onCheckoutRef: (ref: Ref) -> Unit,
|
||||||
onMergeBranch: (ref: Ref) -> Unit,
|
onMergeBranch: (ref: Ref) -> Unit,
|
||||||
onDeleteBranch: (ref: Ref) -> Unit,
|
onDeleteBranch: (ref: Ref) -> Unit,
|
||||||
@ -599,78 +687,68 @@ fun CommitMessage(
|
|||||||
) {
|
) {
|
||||||
val textColor = if (selected) {
|
val textColor = if (selected) {
|
||||||
MaterialTheme.colors.primary
|
MaterialTheme.colors.primary
|
||||||
} else
|
} else MaterialTheme.colors.primaryTextColor
|
||||||
MaterialTheme.colors.primaryTextColor
|
|
||||||
|
|
||||||
val secondaryTextColor = if (selected) {
|
val secondaryTextColor = if (selected) {
|
||||||
MaterialTheme.colors.primary
|
MaterialTheme.colors.primary
|
||||||
} else
|
} else MaterialTheme.colors.secondaryTextColor
|
||||||
MaterialTheme.colors.secondaryTextColor
|
|
||||||
|
|
||||||
Column(
|
Row(
|
||||||
modifier = modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.weight(2f))
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(start = 16.dp)
|
||||||
.fillMaxSize(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
) {
|
||||||
refs
|
refs.sortedWith { ref1, ref2 ->
|
||||||
.sortedWith { ref1, ref2 ->
|
if (ref1.isSameBranch(currentBranch)) {
|
||||||
if (ref1.isSameBranch(currentBranch)) {
|
-1
|
||||||
-1
|
} else {
|
||||||
} else {
|
ref1.name.compareTo(ref2.name)
|
||||||
ref1.name.compareTo(ref2.name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.forEach { ref ->
|
}.forEach { ref ->
|
||||||
if (ref.isTag) {
|
if (ref.isTag) {
|
||||||
TagChip(
|
TagChip(
|
||||||
ref = ref,
|
ref = ref,
|
||||||
color = nodeColor,
|
color = nodeColor,
|
||||||
onCheckoutTag = { onCheckoutRef(ref) },
|
onCheckoutTag = { onCheckoutRef(ref) },
|
||||||
onDeleteTag = { onDeleteTag(ref) },
|
onDeleteTag = { onDeleteTag(ref) },
|
||||||
)
|
)
|
||||||
} else if (ref.isBranch) {
|
} else if (ref.isBranch) {
|
||||||
BranchChip(
|
BranchChip(
|
||||||
ref = ref,
|
ref = ref,
|
||||||
color = nodeColor,
|
color = nodeColor,
|
||||||
currentBranch = currentBranch,
|
currentBranch = currentBranch,
|
||||||
isCurrentBranch = ref.isSameBranch(currentBranch),
|
isCurrentBranch = ref.isSameBranch(currentBranch),
|
||||||
onCheckoutBranch = { onCheckoutRef(ref) },
|
onCheckoutBranch = { onCheckoutRef(ref) },
|
||||||
onMergeBranch = { onMergeBranch(ref) },
|
onMergeBranch = { onMergeBranch(ref) },
|
||||||
onDeleteBranch = { onDeleteBranch(ref) },
|
onDeleteBranch = { onDeleteBranch(ref) },
|
||||||
onRebaseBranch = { onRebaseBranch(ref) },
|
onRebaseBranch = { onRebaseBranch(ref) },
|
||||||
onPullRemoteBranch = { onPullRemoteBranch(ref) },
|
onPullRemoteBranch = { onPullRemoteBranch(ref) },
|
||||||
onPushRemoteBranch = { onPushRemoteBranch(ref) },
|
onPushRemoteBranch = { onPushRemoteBranch(ref) },
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Text(
|
|
||||||
text = commit.shortMessage,
|
|
||||||
modifier = Modifier.padding(start = 16.dp),
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = textColor,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.weight(2f))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = commit.committerIdent.`when`.toSmartSystemString(),
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = secondaryTextColor,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Text(
|
||||||
|
text = commit.shortMessage,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = if (matchesSearchFilter == false) secondaryTextColor else textColor,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.weight(2f))
|
Spacer(modifier = Modifier.weight(2f))
|
||||||
}
|
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = commit.committerIdent.`when`.toSmartSystemString(),
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = secondaryTextColor,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@ -679,15 +757,12 @@ fun DividerLog(modifier: Modifier, graphWidth: Dp) {
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = graphWidth)
|
.padding(start = graphWidth)
|
||||||
.width(8.dp)
|
.width(DIVIDER_WIDTH.dp)
|
||||||
.then(modifier)
|
.then(modifier)
|
||||||
.pointerHoverIcon(PointerIconDefaults.Hand)
|
.pointerHoverIcon(PointerIconDefaults.Hand)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxHeight().width(1.dp).background(color = MaterialTheme.colors.primary)
|
||||||
.fillMaxHeight()
|
|
||||||
.width(1.dp)
|
|
||||||
.background(color = MaterialTheme.colors.primary)
|
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -714,8 +789,7 @@ fun CommitsGraphLine(
|
|||||||
val itemPosition = plotCommit.lane.position
|
val itemPosition = plotCommit.lane.position
|
||||||
|
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize()
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
) {
|
||||||
clipRect {
|
clipRect {
|
||||||
if (plotCommit.childCount > 0) {
|
if (plotCommit.childCount > 0) {
|
||||||
@ -761,9 +835,7 @@ fun CommitsGraphLine(
|
|||||||
}
|
}
|
||||||
|
|
||||||
CommitNode(
|
CommitNode(
|
||||||
modifier = Modifier
|
modifier = Modifier.align(Alignment.CenterStart).padding(start = ((itemPosition + 1) * 30 - 15).dp),
|
||||||
.align(Alignment.CenterStart)
|
|
||||||
.padding(start = ((itemPosition + 1) * 30 - 15).dp),
|
|
||||||
plotCommit = plotCommit,
|
plotCommit = plotCommit,
|
||||||
color = nodeColor,
|
color = nodeColor,
|
||||||
)
|
)
|
||||||
@ -777,10 +849,7 @@ fun CommitNode(
|
|||||||
color: Color,
|
color: Color,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier.size(30.dp).border(2.dp, color, shape = CircleShape).clip(CircleShape)
|
||||||
.size(30.dp)
|
|
||||||
.border(2.dp, color, shape = CircleShape)
|
|
||||||
.clip(CircleShape)
|
|
||||||
) {
|
) {
|
||||||
AvatarImage(
|
AvatarImage(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@ -797,17 +866,15 @@ fun UncommitedChangesGraphNode(
|
|||||||
) {
|
) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize()
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
) {
|
||||||
clipRect {
|
clipRect {
|
||||||
|
|
||||||
if (hasPreviousCommits)
|
if (hasPreviousCommits) drawLine(
|
||||||
drawLine(
|
color = colors[0],
|
||||||
color = colors[0],
|
start = Offset(30f, this.center.y),
|
||||||
start = Offset(30f, this.center.y),
|
end = Offset(30f, this.size.height),
|
||||||
end = Offset(30f, this.size.height),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = colors[0],
|
color = colors[0],
|
||||||
@ -910,14 +977,9 @@ fun RefChip(
|
|||||||
endingContent: @Composable () -> Unit = {},
|
endingContent: @Composable () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(horizontal = 4.dp).clip(RoundedCornerShape(16.dp))
|
||||||
.padding(horizontal = 4.dp)
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
.border(width = 2.dp, color = color, shape = RoundedCornerShape(16.dp))
|
.border(width = 2.dp, color = color, shape = RoundedCornerShape(16.dp))
|
||||||
.combinedClickable(
|
.combinedClickable(onDoubleClick = onCheckoutRef, onClick = {})
|
||||||
onDoubleClick = onCheckoutRef,
|
|
||||||
onClick = {}
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
ContextMenuArea(
|
ContextMenuArea(
|
||||||
items = contextMenuItemsList
|
items = contextMenuItemsList
|
||||||
@ -928,9 +990,7 @@ fun RefChip(
|
|||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.background(color = color)) {
|
Box(modifier = Modifier.background(color = color)) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(6.dp).size(14.dp),
|
||||||
.padding(6.dp)
|
|
||||||
.size(14.dp),
|
|
||||||
painter = painterResource(icon),
|
painter = painterResource(icon),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colors.inversePrimaryTextColor,
|
tint = MaterialTheme.colors.inversePrimaryTextColor,
|
||||||
@ -941,8 +1001,7 @@ fun RefChip(
|
|||||||
color = MaterialTheme.colors.primaryTextColor,
|
color = MaterialTheme.colors.primaryTextColor,
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(horizontal = 6.dp)
|
||||||
.padding(horizontal = 6.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
endingContent()
|
endingContent()
|
||||||
|
@ -1,16 +1,29 @@
|
|||||||
package app.viewmodels
|
package app.viewmodels
|
||||||
|
|
||||||
import app.extensions.simpleName
|
|
||||||
import app.git.*
|
import app.git.*
|
||||||
import app.git.graph.GraphCommitList
|
import app.git.graph.GraphCommitList
|
||||||
|
import app.git.graph.GraphNode
|
||||||
import app.ui.SelectedItem
|
import app.ui.SelectedItem
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.lib.Ref
|
import org.eclipse.jgit.lib.Ref
|
||||||
import org.eclipse.jgit.revwalk.RevCommit
|
import org.eclipse.jgit.revwalk.RevCommit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents when the search filter is not being used or the results list is empty
|
||||||
|
*/
|
||||||
|
private const val NONE_MATCHING_INDEX = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The search UI starts the index count at 1 (for example "1/10" to represent the first commit of the search result
|
||||||
|
* being selected)
|
||||||
|
*/
|
||||||
|
private const val FIRST_INDEX = 1
|
||||||
|
|
||||||
class LogViewModel @Inject constructor(
|
class LogViewModel @Inject constructor(
|
||||||
private val logManager: LogManager,
|
private val logManager: LogManager,
|
||||||
private val statusManager: StatusManager,
|
private val statusManager: StatusManager,
|
||||||
@ -27,6 +40,15 @@ class LogViewModel @Inject constructor(
|
|||||||
val logStatus: StateFlow<LogStatus>
|
val logStatus: StateFlow<LogStatus>
|
||||||
get() = _logStatus
|
get() = _logStatus
|
||||||
|
|
||||||
|
var savedSearchFilter: String = ""
|
||||||
|
|
||||||
|
private val _focusCommit = MutableSharedFlow<GraphNode>()
|
||||||
|
val focusCommit: SharedFlow<GraphNode> = _focusCommit
|
||||||
|
|
||||||
|
|
||||||
|
private val _logSearchFilterResults = MutableStateFlow<LogSearch>(LogSearch.NotSearching)
|
||||||
|
val logSearchFilterResults: StateFlow<LogSearch> = _logSearchFilterResults
|
||||||
|
|
||||||
private suspend fun loadLog(git: Git) {
|
private suspend fun loadLog(git: Git) {
|
||||||
_logStatus.value = LogStatus.Loading
|
_logStatus.value = LogStatus.Loading
|
||||||
|
|
||||||
@ -38,13 +60,18 @@ class LogViewModel @Inject constructor(
|
|||||||
repositoryState = repositoryManager.getRepositoryState(git),
|
repositoryState = repositoryManager.getRepositoryState(git),
|
||||||
)
|
)
|
||||||
|
|
||||||
val hasUncommitedChanges = statusSummary.addedCount + statusSummary.deletedCount + statusSummary.modifiedCount > 0
|
val hasUncommitedChanges =
|
||||||
|
statusSummary.addedCount + statusSummary.deletedCount + statusSummary.modifiedCount > 0
|
||||||
val log = logManager.loadLog(git, currentBranch, hasUncommitedChanges)
|
val log = logManager.loadLog(git, currentBranch, hasUncommitedChanges)
|
||||||
|
|
||||||
_logStatus.value = LogStatus.Loaded(hasUncommitedChanges, log, currentBranch, statusSummary)
|
_logStatus.value = LogStatus.Loaded(hasUncommitedChanges, log, currentBranch, statusSummary)
|
||||||
|
|
||||||
|
// Remove search filter if the log has been updated
|
||||||
|
// TODO: Should we just update the search instead of closing it?
|
||||||
|
_logSearchFilterResults.value = LogSearch.NotSearching
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pushToRemoteBranch(branch: Ref) = tabState.safeProcessing(
|
fun pushToRemoteBranch(branch: Ref) = tabState.safeProcessing(
|
||||||
refreshType = RefreshType.ALL_DATA,
|
refreshType = RefreshType.ALL_DATA,
|
||||||
) { git ->
|
) { git ->
|
||||||
remoteOperationsManager.pushToBranch(
|
remoteOperationsManager.pushToBranch(
|
||||||
@ -55,7 +82,7 @@ class LogViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullFromRemoteBranch(branch: Ref) = tabState.safeProcessing(
|
fun pullFromRemoteBranch(branch: Ref) = tabState.safeProcessing(
|
||||||
refreshType = RefreshType.ALL_DATA,
|
refreshType = RefreshType.ALL_DATA,
|
||||||
) { git ->
|
) { git ->
|
||||||
remoteOperationsManager.pullFromBranch(
|
remoteOperationsManager.pullFromBranch(
|
||||||
@ -166,8 +193,117 @@ class LogViewModel @Inject constructor(
|
|||||||
rebaseManager.rebaseBranch(git, ref)
|
rebaseManager.rebaseBranch(git, ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectLogLine(selectedItem: SelectedItem) {
|
fun selectUncommitedChanges() {
|
||||||
tabState.newSelectedItem(selectedItem)
|
tabState.newSelectedItem(SelectedItem.UncommitedChanges)
|
||||||
|
|
||||||
|
val searchValue = _logSearchFilterResults.value
|
||||||
|
if (searchValue is LogSearch.SearchResults) {
|
||||||
|
val lastIndexSelected = getLastIndexSelected()
|
||||||
|
|
||||||
|
_logSearchFilterResults.value = searchValue.copy(index = lastIndexSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLastIndexSelected(): Int {
|
||||||
|
val logSearchFilterResultsValue = logSearchFilterResults.value
|
||||||
|
|
||||||
|
return if (logSearchFilterResultsValue is LogSearch.SearchResults) {
|
||||||
|
logSearchFilterResultsValue.index
|
||||||
|
} else
|
||||||
|
NONE_MATCHING_INDEX
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectLogLine(commit: GraphNode) {
|
||||||
|
tabState.newSelectedItem(SelectedItem.Commit(commit))
|
||||||
|
|
||||||
|
val searchValue = _logSearchFilterResults.value
|
||||||
|
if (searchValue is LogSearch.SearchResults) {
|
||||||
|
var index = searchValue.commits.indexOf(commit)
|
||||||
|
|
||||||
|
if (index == -1)
|
||||||
|
index = getLastIndexSelected()
|
||||||
|
else
|
||||||
|
index += 1 // +1 because UI count starts at 1
|
||||||
|
|
||||||
|
_logSearchFilterResults.value = searchValue.copy(index = index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onSearchValueChanged(searchTerm: String) {
|
||||||
|
val logStatusValue = logStatus.value
|
||||||
|
|
||||||
|
if (logStatusValue !is LogStatus.Loaded)
|
||||||
|
return
|
||||||
|
|
||||||
|
savedSearchFilter = searchTerm
|
||||||
|
|
||||||
|
if (searchTerm.isNotBlank()) {
|
||||||
|
val lowercaseValue = searchTerm.lowercase()
|
||||||
|
val plotCommitList = logStatusValue.plotCommitList
|
||||||
|
|
||||||
|
val matchingCommits = plotCommitList.filter {
|
||||||
|
it.fullMessage.lowercase().contains(lowercaseValue) ||
|
||||||
|
it.authorIdent.name.lowercase().contains(lowercaseValue) ||
|
||||||
|
it.committerIdent.name.lowercase().contains(lowercaseValue) ||
|
||||||
|
it.name.lowercase().contains(lowercaseValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
var startingUiIndex = NONE_MATCHING_INDEX
|
||||||
|
|
||||||
|
if (matchingCommits.isNotEmpty()) {
|
||||||
|
_focusCommit.emit(matchingCommits.first())
|
||||||
|
startingUiIndex = FIRST_INDEX
|
||||||
|
}
|
||||||
|
|
||||||
|
_logSearchFilterResults.value = LogSearch.SearchResults(matchingCommits, startingUiIndex)
|
||||||
|
} else
|
||||||
|
_logSearchFilterResults.value = LogSearch.SearchResults(emptyList(), NONE_MATCHING_INDEX)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun selectPreviousFilterCommit() {
|
||||||
|
val logSearchFilterResultsValue = logSearchFilterResults.value
|
||||||
|
|
||||||
|
if(logSearchFilterResultsValue !is LogSearch.SearchResults) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val index = logSearchFilterResultsValue.index
|
||||||
|
val commits = logSearchFilterResultsValue.commits
|
||||||
|
|
||||||
|
if(index == NONE_MATCHING_INDEX || index == FIRST_INDEX)
|
||||||
|
return
|
||||||
|
|
||||||
|
val newIndex = index - 1
|
||||||
|
val newCommitToSelect = commits[newIndex - 1]
|
||||||
|
|
||||||
|
_logSearchFilterResults.value = logSearchFilterResultsValue.copy(index = newIndex)
|
||||||
|
_focusCommit.emit(newCommitToSelect)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun selectNextFilterCommit() {
|
||||||
|
val logSearchFilterResultsValue = logSearchFilterResults.value
|
||||||
|
|
||||||
|
if(logSearchFilterResultsValue !is LogSearch.SearchResults) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val index = logSearchFilterResultsValue.index
|
||||||
|
val commits = logSearchFilterResultsValue.commits
|
||||||
|
val totalCount = logSearchFilterResultsValue.totalCount
|
||||||
|
|
||||||
|
if(index == NONE_MATCHING_INDEX || index == totalCount)
|
||||||
|
return
|
||||||
|
|
||||||
|
val newIndex = index + 1
|
||||||
|
// Use index instead of newIndex because Kotlin arrays start at 0 while the UI count starts at 1
|
||||||
|
val newCommitToSelect = commits[index]
|
||||||
|
|
||||||
|
_logSearchFilterResults.value = logSearchFilterResultsValue.copy(index = newIndex)
|
||||||
|
_focusCommit.emit(newCommitToSelect)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeSearch() {
|
||||||
|
_logSearchFilterResults.value = LogSearch.NotSearching
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,3 +316,12 @@ sealed class LogStatus {
|
|||||||
val statusSummary: StatusSummary,
|
val statusSummary: StatusSummary,
|
||||||
) : LogStatus()
|
) : LogStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class LogSearch {
|
||||||
|
object NotSearching : LogSearch()
|
||||||
|
data class SearchResults(
|
||||||
|
val commits: List<GraphNode>,
|
||||||
|
val index: Int,
|
||||||
|
val totalCount: Int = commits.count(),
|
||||||
|
) : LogSearch()
|
||||||
|
}
|
||||||
|
@ -22,14 +22,7 @@ class StatusViewModel @Inject constructor(
|
|||||||
private val _stageStatus = MutableStateFlow<StageStatus>(StageStatus.Loaded(listOf(), listOf()))
|
private val _stageStatus = MutableStateFlow<StageStatus>(StageStatus.Loaded(listOf(), listOf()))
|
||||||
val stageStatus: StateFlow<StageStatus> = _stageStatus
|
val stageStatus: StateFlow<StageStatus> = _stageStatus
|
||||||
|
|
||||||
private val _commitMessage = MutableStateFlow("")
|
var savedCommitMessage: String = ""
|
||||||
val commitMessage: StateFlow<String> = _commitMessage
|
|
||||||
var newCommitMessage: String
|
|
||||||
get() = commitMessage.value
|
|
||||||
set(value) {
|
|
||||||
_commitMessage.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasPreviousCommits = true // When false, disable "amend previous commit"
|
var hasPreviousCommits = true // When false, disable "amend previous commit"
|
||||||
|
|
||||||
private var lastUncommitedChangesState = false
|
private var lastUncommitedChangesState = false
|
||||||
|
Loading…
Reference in New Issue
Block a user