parent
91094a8771
commit
9dfd5073bd
@ -17,6 +17,7 @@ object AppIcons {
|
||||
const val COPY = "copy.svg"
|
||||
const val CUT = "cut.svg"
|
||||
const val DELETE = "delete.svg"
|
||||
const val DONE = "done.svg"
|
||||
const val DOWNLOAD = "download.svg"
|
||||
const val DROPDOWN = "dropdown.svg"
|
||||
const val ERROR = "error.svg"
|
||||
@ -37,6 +38,7 @@ object AppIcons {
|
||||
const val PERSON = "person.svg"
|
||||
const val REFRESH = "refresh.svg"
|
||||
const val REMOVE = "remove.svg"
|
||||
const val REMOVE_DONE = "remove_done.svg"
|
||||
const val REVERT = "revert.svg"
|
||||
const val SEARCH = "search.svg"
|
||||
const val SETTINGS = "settings.svg"
|
||||
|
@ -11,11 +11,11 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -26,7 +26,7 @@ import com.jetpackduba.gitnuro.theme.*
|
||||
import com.jetpackduba.gitnuro.ui.components.*
|
||||
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
|
||||
import com.jetpackduba.gitnuro.ui.context_menu.commitedChangesEntriesContextMenuItems
|
||||
import com.jetpackduba.gitnuro.viewmodels.CommitChangesStatus
|
||||
import com.jetpackduba.gitnuro.viewmodels.CommitChangesState
|
||||
import com.jetpackduba.gitnuro.viewmodels.CommitChangesViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@ -48,21 +48,36 @@ fun CommitChanges(
|
||||
commitChangesViewModel.loadChanges(selectedItem.revCommit)
|
||||
}
|
||||
|
||||
val commitChangesStatusState = commitChangesViewModel.commitChangesStatus.collectAsState()
|
||||
val commitChangesStatus = commitChangesViewModel.commitChangesState.collectAsState().value
|
||||
val showSearch by commitChangesViewModel.showSearch.collectAsState()
|
||||
|
||||
when (val commitChangesStatus = commitChangesStatusState.value) {
|
||||
CommitChangesStatus.Loading -> {
|
||||
|
||||
var searchFilter by remember(commitChangesViewModel, showSearch, commitChangesStatus) {
|
||||
mutableStateOf(commitChangesViewModel.searchFilter.value)
|
||||
}
|
||||
|
||||
when (commitChangesStatus) {
|
||||
CommitChangesState.Loading -> {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant)
|
||||
}
|
||||
|
||||
is CommitChangesStatus.Loaded -> {
|
||||
is CommitChangesState.Loaded -> {
|
||||
CommitChangesView(
|
||||
diffSelected = diffSelected,
|
||||
commit = commitChangesStatus.commit,
|
||||
changes = commitChangesStatus.changes,
|
||||
changes = commitChangesStatus.changesFiltered,
|
||||
onDiffSelected = onDiffSelected,
|
||||
onBlame = onBlame,
|
||||
onHistory = onHistory,
|
||||
showSearch = showSearch,
|
||||
searchFilter = searchFilter,
|
||||
onSearchFilterToggled = { visible ->
|
||||
commitChangesViewModel.onSearchFilterToggled(visible)
|
||||
},
|
||||
onSearchFilterChanged = { filter ->
|
||||
searchFilter = filter
|
||||
commitChangesViewModel.onSearchFilterChanged(filter)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -76,15 +91,23 @@ fun CommitChangesView(
|
||||
diffSelected: DiffEntryType?,
|
||||
onBlame: (String) -> Unit,
|
||||
onHistory: (String) -> Unit,
|
||||
showSearch: Boolean,
|
||||
searchFilter: TextFieldValue,
|
||||
onSearchFilterToggled: (Boolean) -> Unit,
|
||||
onSearchFilterChanged: (TextFieldValue) -> Unit,
|
||||
) {
|
||||
|
||||
/**
|
||||
* State used to prevent the text field from getting the focus when returning from another tab
|
||||
*/
|
||||
var requestFocus by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp, bottom = 8.dp)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
val scroll = rememberScrollState(0)
|
||||
var showSearch by remember(commit) { mutableStateOf(false) }
|
||||
var searchFilter by remember(commit) { mutableStateOf("") } // TODO Persist in viewmodel
|
||||
val searchFocusRequester = remember { FocusRequester() }
|
||||
|
||||
Column(
|
||||
@ -116,43 +139,40 @@ fun CommitChangesView(
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
showSearch = !showSearch
|
||||
}
|
||||
onSearchFilterToggled(!showSearch)
|
||||
|
||||
if (!showSearch)
|
||||
requestFocus = true
|
||||
},
|
||||
modifier = Modifier.handOnHover(),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(AppIcons.SEARCH),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSearch) {
|
||||
AdjustableOutlinedTextField(
|
||||
value = searchFilter,
|
||||
onValueChange = {
|
||||
searchFilter = it
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.focusable()
|
||||
.focusRequester(searchFocusRequester)
|
||||
SearchTextField(
|
||||
searchFilter = searchFilter,
|
||||
onSearchFilterChanged = onSearchFilterChanged,
|
||||
searchFocusRequester = searchFocusRequester,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(showSearch) {
|
||||
if (showSearch) {
|
||||
LaunchedEffect(showSearch, requestFocus) {
|
||||
if (showSearch && requestFocus) {
|
||||
searchFocusRequester.requestFocus()
|
||||
requestFocus = false
|
||||
}
|
||||
}
|
||||
|
||||
CommitLogChanges(
|
||||
diffSelected = diffSelected,
|
||||
diffEntries = if (showSearch && searchFilter.isNotBlank()) changes.filter {
|
||||
it.filePath.lowercaseContains(
|
||||
searchFilter
|
||||
)
|
||||
} else changes,
|
||||
diffEntries = changes,
|
||||
onDiffSelected = onDiffSelected,
|
||||
onBlame = onBlame,
|
||||
onHistory = onHistory,
|
||||
|
@ -8,6 +8,7 @@ import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
@ -23,14 +24,19 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.jetpackduba.gitnuro.AppIcons
|
||||
import com.jetpackduba.gitnuro.extensions.*
|
||||
import com.jetpackduba.gitnuro.git.DiffEntryType
|
||||
import com.jetpackduba.gitnuro.git.workspace.StatusEntry
|
||||
@ -38,16 +44,14 @@ import com.jetpackduba.gitnuro.git.workspace.StatusType
|
||||
import com.jetpackduba.gitnuro.keybindings.KeybindingOption
|
||||
import com.jetpackduba.gitnuro.keybindings.matchesBinding
|
||||
import com.jetpackduba.gitnuro.theme.*
|
||||
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
|
||||
import com.jetpackduba.gitnuro.ui.components.SecondaryButton
|
||||
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
|
||||
import com.jetpackduba.gitnuro.ui.components.*
|
||||
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
|
||||
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement
|
||||
import com.jetpackduba.gitnuro.ui.context_menu.EntryType
|
||||
import com.jetpackduba.gitnuro.ui.context_menu.statusEntriesContextMenuItems
|
||||
import com.jetpackduba.gitnuro.ui.dialogs.CommitAuthorDialog
|
||||
import com.jetpackduba.gitnuro.viewmodels.CommitterDataRequestState
|
||||
import com.jetpackduba.gitnuro.viewmodels.StageStatus
|
||||
import com.jetpackduba.gitnuro.viewmodels.StageState
|
||||
import com.jetpackduba.gitnuro.viewmodels.StatusViewModel
|
||||
import org.eclipse.jgit.lib.RepositoryState
|
||||
|
||||
@ -61,7 +65,7 @@ fun UncommitedChanges(
|
||||
onBlameFile: (String) -> Unit,
|
||||
onHistoryFile: (String) -> Unit,
|
||||
) {
|
||||
val stageStatusState = statusViewModel.stageStatus.collectAsState()
|
||||
val stageStatus = statusViewModel.stageState.collectAsState().value
|
||||
var commitMessage by remember(statusViewModel) { mutableStateOf(statusViewModel.savedCommitMessage.message) }
|
||||
val stagedListState by statusViewModel.stagedLazyListState.collectAsState()
|
||||
val unstagedListState by statusViewModel.unstagedLazyListState.collectAsState()
|
||||
@ -69,14 +73,18 @@ fun UncommitedChanges(
|
||||
val committerDataRequestState = statusViewModel.committerDataRequestState.collectAsState()
|
||||
val committerDataRequestStateValue = committerDataRequestState.value
|
||||
|
||||
val stageStatus = stageStatusState.value
|
||||
val showSearchStaged by statusViewModel.showSearchStaged.collectAsState()
|
||||
val searchFilterStaged by statusViewModel.searchFilterStaged.collectAsState()
|
||||
val showSearchUnstaged by statusViewModel.showSearchUnstaged.collectAsState()
|
||||
val searchFilterUnstaged by statusViewModel.searchFilterUnstaged.collectAsState()
|
||||
|
||||
val staged: List<StatusEntry>
|
||||
val unstaged: List<StatusEntry>
|
||||
val isLoading: Boolean
|
||||
|
||||
if (stageStatus is StageStatus.Loaded) {
|
||||
staged = stageStatus.staged
|
||||
unstaged = stageStatus.unstaged
|
||||
if (stageStatus is StageState.Loaded) {
|
||||
staged = stageStatus.stagedFiltered
|
||||
unstaged = stageStatus.unstagedFiltered
|
||||
isLoading = stageStatus.isPartiallyReloading
|
||||
} else {
|
||||
staged = listOf()
|
||||
@ -133,12 +141,21 @@ fun UncommitedChanges(
|
||||
title = "Staged",
|
||||
allActionTitle = "Unstage all",
|
||||
actionTitle = "Unstage",
|
||||
actionIcon = AppIcons.REMOVE_DONE,
|
||||
selectedEntryType = if (selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null,
|
||||
actionColor = MaterialTheme.colors.error,
|
||||
actionTextColor = MaterialTheme.colors.onError,
|
||||
statusEntries = staged,
|
||||
lazyListState = stagedListState,
|
||||
onDiffEntrySelected = onStagedDiffEntrySelected,
|
||||
showSearch = showSearchStaged,
|
||||
searchFilter = searchFilterStaged,
|
||||
onSearchFilterToggled = {
|
||||
statusViewModel.onSearchFilterToggledStaged(it)
|
||||
},
|
||||
onSearchFilterChanged = {
|
||||
statusViewModel.onSearchFilterChangedStaged(it)
|
||||
},
|
||||
onDiffEntryOptionSelected = {
|
||||
statusViewModel.unstage(it)
|
||||
},
|
||||
@ -163,12 +180,21 @@ fun UncommitedChanges(
|
||||
.fillMaxWidth(),
|
||||
title = "Unstaged",
|
||||
actionTitle = "Stage",
|
||||
actionIcon = AppIcons.DONE,
|
||||
selectedEntryType = if (selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null,
|
||||
actionColor = MaterialTheme.colors.primary,
|
||||
actionTextColor = MaterialTheme.colors.onPrimary,
|
||||
statusEntries = unstaged,
|
||||
lazyListState = unstagedListState,
|
||||
onDiffEntrySelected = onUnstagedDiffEntrySelected,
|
||||
showSearch = showSearchUnstaged,
|
||||
searchFilter = searchFilterUnstaged,
|
||||
onSearchFilterToggled = {
|
||||
statusViewModel.onSearchFilterToggledUnstaged(it)
|
||||
},
|
||||
onSearchFilterChanged = {
|
||||
statusViewModel.onSearchFilterChangedUnstaged(it)
|
||||
},
|
||||
onDiffEntryOptionSelected = {
|
||||
statusViewModel.stage(it)
|
||||
},
|
||||
@ -558,6 +584,11 @@ private fun EntriesList(
|
||||
actionTitle: String,
|
||||
actionColor: Color,
|
||||
actionTextColor: Color,
|
||||
actionIcon: String,
|
||||
showSearch: Boolean,
|
||||
searchFilter: TextFieldValue,
|
||||
onSearchFilterToggled: (Boolean) -> Unit,
|
||||
onSearchFilterChanged: (TextFieldValue) -> Unit,
|
||||
statusEntries: List<StatusEntry>,
|
||||
lazyListState: LazyListState,
|
||||
onDiffEntrySelected: (StatusEntry) -> Unit,
|
||||
@ -567,19 +598,30 @@ private fun EntriesList(
|
||||
allActionTitle: String,
|
||||
selectedEntryType: DiffEntryType?,
|
||||
) {
|
||||
val searchFocusRequester = remember { FocusRequester() }
|
||||
val headerHoverInteraction = remember { MutableInteractionSource() }
|
||||
val isHeaderHovered by headerHoverInteraction.collectIsHoveredAsState()
|
||||
|
||||
/**
|
||||
* State used to prevent the text field from getting the focus when returning from another tab
|
||||
*/
|
||||
var requestFocus by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
Box(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(34.dp)
|
||||
.fillMaxWidth()
|
||||
.background(color = MaterialTheme.colors.tertiarySurface)
|
||||
.hoverable(headerHoverInteraction),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
.padding(start = 16.dp, end = 8.dp)
|
||||
.weight(1f),
|
||||
text = title,
|
||||
fontWeight = FontWeight.Normal,
|
||||
textAlign = TextAlign.Left,
|
||||
@ -588,15 +630,49 @@ private fun EntriesList(
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
SecondaryButton(
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
IconButton(
|
||||
onClick = {
|
||||
onSearchFilterToggled(!showSearch)
|
||||
|
||||
if (!showSearch)
|
||||
requestFocus = true
|
||||
},
|
||||
modifier = Modifier.handOnHover()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(AppIcons.SEARCH),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
)
|
||||
}
|
||||
|
||||
SecondaryButtonCompactable(
|
||||
text = allActionTitle,
|
||||
icon = actionIcon,
|
||||
isParentHovered = isHeaderHovered,
|
||||
backgroundButton = actionColor,
|
||||
textColor = actionTextColor,
|
||||
onClick = onAllAction
|
||||
onClick = onAllAction,
|
||||
modifier = Modifier.padding(start = 4.dp, end = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
if (showSearch) {
|
||||
SearchTextField(
|
||||
searchFilter = searchFilter,
|
||||
onSearchFilterChanged = onSearchFilterChanged,
|
||||
searchFocusRequester = searchFocusRequester,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(showSearch, requestFocus) {
|
||||
if (showSearch && requestFocus) {
|
||||
searchFocusRequester.requestFocus()
|
||||
requestFocus = false
|
||||
}
|
||||
}
|
||||
|
||||
ScrollableLazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -126,3 +127,103 @@ fun AdjustableOutlinedTextField(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AdjustableOutlinedTextField(
|
||||
value: TextFieldValue,
|
||||
hint: String = "",
|
||||
onValueChange: (TextFieldValue) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
isError: Boolean = false,
|
||||
singleLine: Boolean = false,
|
||||
colors: TextFieldColors = outlinedTextFieldColors(),
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
textStyle: TextStyle = LocalTextStyle.current.copy(
|
||||
fontSize = MaterialTheme.typography.body1.fontSize,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = RoundedCornerShape(4.dp),
|
||||
backgroundColor: Color = MaterialTheme.colors.background,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
val textColor = textStyle.color.takeOrElse {
|
||||
colors.textColor(enabled).value
|
||||
}
|
||||
|
||||
val cursorColor = colors.cursorColor(isError).value
|
||||
val indicatorColor by colors.indicatorColor(enabled, isError, interactionSource)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
) {
|
||||
BasicTextField(
|
||||
modifier = Modifier
|
||||
.heightIn(min = 38.dp)
|
||||
.background(backgroundColor)
|
||||
.fillMaxWidth(),
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
enabled = enabled,
|
||||
maxLines = maxLines,
|
||||
textStyle = textStyle.copy(color = textColor),
|
||||
interactionSource = interactionSource,
|
||||
keyboardOptions = keyboardOptions,
|
||||
cursorBrush = SolidColor(cursorColor),
|
||||
singleLine = singleLine,
|
||||
visualTransformation = visualTransformation,
|
||||
decorationBox = { innerTextField: @Composable () -> Unit ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = indicatorColor,
|
||||
shape = shape
|
||||
)
|
||||
.padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (leadingIcon != null) {
|
||||
leadingIcon()
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
innerTextField()
|
||||
}
|
||||
|
||||
|
||||
if (trailingIcon != null) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
trailingIcon()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = 12.dp),
|
||||
) {
|
||||
if (leadingIcon != null) {
|
||||
leadingIcon()
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
|
||||
if (value.text.isEmpty() && hint.isNotEmpty()) {
|
||||
Text(
|
||||
hint,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colors.onBackgroundSecondary,
|
||||
style = MaterialTheme.typography.body2
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -146,7 +146,8 @@ fun RepositoriesTabPanel(
|
||||
fun Tab(
|
||||
title: MutableState<String>,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit, onCloseTab: () -> Unit
|
||||
onClick: () -> Unit,
|
||||
onCloseTab: () -> Unit,
|
||||
) {
|
||||
val backgroundColor = if (isSelected)
|
||||
MaterialTheme.colors.surface
|
||||
|
@ -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
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.jetpackduba.gitnuro.extensions.handMouseClickable
|
||||
import com.jetpackduba.gitnuro.theme.tertiarySurface
|
||||
|
||||
@Composable
|
||||
fun SecondaryButton(
|
||||
@ -24,8 +36,7 @@ fun SecondaryButton(
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(RoundedCornerShape(5.dp))
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(backgroundButton)
|
||||
.handMouseClickable { onClick() },
|
||||
) {
|
||||
@ -38,3 +49,72 @@ fun SecondaryButton(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SecondaryButtonCompactable(
|
||||
modifier: Modifier = Modifier,
|
||||
icon: String,
|
||||
text: String,
|
||||
textColor: Color = MaterialTheme.colors.onPrimary,
|
||||
backgroundButton: Color,
|
||||
maxLines: Int = 1,
|
||||
isParentHovered: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val hoverInteraction = remember { MutableInteractionSource() }
|
||||
val isHovered by hoverInteraction.collectIsHoveredAsState()
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(isHovered, isParentHovered) {
|
||||
isExpanded = when {
|
||||
isHovered -> true
|
||||
isExpanded && isParentHovered -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val targetBackground: Color
|
||||
val iconPadding: Float
|
||||
|
||||
if (isExpanded) {
|
||||
targetBackground = backgroundButton
|
||||
iconPadding = 12f
|
||||
} else {
|
||||
targetBackground = MaterialTheme.colors.tertiarySurface
|
||||
iconPadding = 0f
|
||||
}
|
||||
|
||||
val color by animateColorAsState(targetBackground)
|
||||
val iconPaddingState by animateFloatAsState(iconPadding)
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(color)
|
||||
.hoverable(hoverInteraction)
|
||||
.handMouseClickable { onClick() }
|
||||
.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painterResource(icon),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier
|
||||
.padding(start = iconPaddingState.dp, end = 8.dp)
|
||||
.size(18.dp)
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = textColor,
|
||||
maxLines = maxLines,
|
||||
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp, end = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -626,13 +626,15 @@ fun HunkHeader(
|
||||
text = "Discard hunk",
|
||||
backgroundButton = MaterialTheme.colors.error,
|
||||
textColor = MaterialTheme.colors.onError,
|
||||
onClick = onResetHunk
|
||||
onClick = onResetHunk,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
SecondaryButton(
|
||||
text = buttonText,
|
||||
backgroundButton = color,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
onClick = {
|
||||
if (diffEntryType is DiffEntryType.StagedDiff) {
|
||||
onUnstageHunk()
|
||||
@ -787,6 +789,7 @@ fun UncommitedDiffFileHeaderButtons(
|
||||
SecondaryButton(
|
||||
text = buttonText,
|
||||
backgroundButton = color,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
onClick = {
|
||||
if (diffEntryType is DiffEntryType.StagedDiff) {
|
||||
onUnstageFile(diffEntryType.statusEntry)
|
||||
|
@ -1,11 +1,14 @@
|
||||
package com.jetpackduba.gitnuro.viewmodels
|
||||
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.jetpackduba.gitnuro.extensions.delayedStateChange
|
||||
import com.jetpackduba.gitnuro.extensions.filePath
|
||||
import com.jetpackduba.gitnuro.extensions.lowercaseContains
|
||||
import com.jetpackduba.gitnuro.git.RefreshType
|
||||
import com.jetpackduba.gitnuro.git.TabState
|
||||
import com.jetpackduba.gitnuro.git.diff.GetCommitDiffEntriesUseCase
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.eclipse.jgit.diff.DiffEntry
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
import javax.inject.Inject
|
||||
@ -15,26 +18,69 @@ private const val MIN_TIME_IN_MS_TO_SHOW_LOAD = 300L
|
||||
class CommitChangesViewModel @Inject constructor(
|
||||
private val tabState: TabState,
|
||||
private val getCommitDiffEntriesUseCase: GetCommitDiffEntriesUseCase,
|
||||
tabScope: CoroutineScope,
|
||||
) {
|
||||
private val _commitChangesStatus = MutableStateFlow<CommitChangesStatus>(CommitChangesStatus.Loading)
|
||||
val commitChangesStatus: StateFlow<CommitChangesStatus> = _commitChangesStatus
|
||||
private val _showSearch = MutableStateFlow(false)
|
||||
val showSearch: StateFlow<Boolean> = _showSearch
|
||||
|
||||
private val _searchFilter = MutableStateFlow(TextFieldValue(""))
|
||||
val searchFilter: StateFlow<TextFieldValue> = _searchFilter
|
||||
|
||||
private val _commitChangesState = MutableStateFlow<CommitChangesState>(CommitChangesState.Loading)
|
||||
val commitChangesState: StateFlow<CommitChangesState> =
|
||||
combine(_commitChangesState, _showSearch, _searchFilter) { state, showSearch, filter ->
|
||||
if (state is CommitChangesState.Loaded) {
|
||||
if (showSearch && filter.text.isNotBlank()) {
|
||||
state.copy(changesFiltered = state.changes.filter { it.filePath.lowercaseContains(filter.text) })
|
||||
} else {
|
||||
state
|
||||
}
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}.stateIn(
|
||||
tabScope,
|
||||
SharingStarted.Lazily,
|
||||
CommitChangesState.Loading
|
||||
)
|
||||
|
||||
|
||||
fun loadChanges(commit: RevCommit) = tabState.runOperation(
|
||||
refreshType = RefreshType.NONE,
|
||||
) { git ->
|
||||
delayedStateChange(
|
||||
delayMs = MIN_TIME_IN_MS_TO_SHOW_LOAD,
|
||||
onDelayTriggered = { _commitChangesStatus.value = CommitChangesStatus.Loading }
|
||||
) {
|
||||
val changes = getCommitDiffEntriesUseCase(git, commit)
|
||||
val state = _commitChangesState.value
|
||||
|
||||
_commitChangesStatus.value = CommitChangesStatus.Loaded(commit, changes)
|
||||
// Check if it's a different commit before resetting everything
|
||||
if (
|
||||
state is CommitChangesState.Loading ||
|
||||
state is CommitChangesState.Loaded && state.commit != commit
|
||||
) {
|
||||
delayedStateChange(
|
||||
delayMs = MIN_TIME_IN_MS_TO_SHOW_LOAD,
|
||||
onDelayTriggered = { _commitChangesState.value = CommitChangesState.Loading }
|
||||
) {
|
||||
val changes = getCommitDiffEntriesUseCase(git, commit)
|
||||
|
||||
_commitChangesState.value = CommitChangesState.Loaded(commit, changes, changes)
|
||||
}
|
||||
|
||||
_showSearch.value = false
|
||||
_searchFilter.value = TextFieldValue("")
|
||||
}
|
||||
}
|
||||
|
||||
fun onSearchFilterToggled(visible: Boolean) {
|
||||
_showSearch.value = visible
|
||||
}
|
||||
|
||||
fun onSearchFilterChanged(filter: TextFieldValue) {
|
||||
_searchFilter.value = filter
|
||||
}
|
||||
}
|
||||
|
||||
sealed class CommitChangesStatus {
|
||||
object Loading : CommitChangesStatus()
|
||||
data class Loaded(val commit: RevCommit, val changes: List<DiffEntry>) : CommitChangesStatus()
|
||||
sealed class CommitChangesState {
|
||||
object Loading : CommitChangesState()
|
||||
data class Loaded(val commit: RevCommit, val changes: List<DiffEntry>, val changesFiltered: List<DiffEntry>) :
|
||||
CommitChangesState()
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
package com.jetpackduba.gitnuro.viewmodels
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.jetpackduba.gitnuro.extensions.delayedStateChange
|
||||
import com.jetpackduba.gitnuro.extensions.isMerging
|
||||
import com.jetpackduba.gitnuro.extensions.isReverting
|
||||
import com.jetpackduba.gitnuro.extensions.lowercaseContains
|
||||
import com.jetpackduba.gitnuro.git.RefreshType
|
||||
import com.jetpackduba.gitnuro.git.TabState
|
||||
import com.jetpackduba.gitnuro.git.author.LoadAuthorUseCase
|
||||
@ -18,10 +20,7 @@ import com.jetpackduba.gitnuro.git.workspace.*
|
||||
import com.jetpackduba.gitnuro.models.AuthorInfo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.eclipse.jgit.api.Git
|
||||
@ -54,8 +53,50 @@ class StatusViewModel @Inject constructor(
|
||||
private val saveAuthorUseCase: SaveAuthorUseCase,
|
||||
private val tabScope: CoroutineScope,
|
||||
) {
|
||||
private val _stageStatus = MutableStateFlow<StageStatus>(StageStatus.Loaded(listOf(), listOf(), false))
|
||||
val stageStatus: StateFlow<StageStatus> = _stageStatus
|
||||
private val _showSearchUnstaged = MutableStateFlow(false)
|
||||
val showSearchUnstaged: StateFlow<Boolean> = _showSearchUnstaged
|
||||
|
||||
private val _showSearchStaged = MutableStateFlow(false)
|
||||
val showSearchStaged: StateFlow<Boolean> = _showSearchStaged
|
||||
|
||||
private val _searchFilterUnstaged = MutableStateFlow(TextFieldValue(""))
|
||||
val searchFilterUnstaged: StateFlow<TextFieldValue> = _searchFilterUnstaged
|
||||
|
||||
private val _searchFilterStaged = MutableStateFlow(TextFieldValue(""))
|
||||
val searchFilterStaged: StateFlow<TextFieldValue> = _searchFilterStaged
|
||||
|
||||
private val _stageState = MutableStateFlow<StageState>(StageState.Loading)
|
||||
|
||||
val stageState: StateFlow<StageState> = combine(
|
||||
_stageState,
|
||||
_showSearchStaged,
|
||||
_searchFilterStaged,
|
||||
_showSearchUnstaged,
|
||||
_searchFilterUnstaged,
|
||||
) { state, showSearchStaged, filterStaged, showSearchUnstaged, filterUnstaged ->
|
||||
if (state is StageState.Loaded) {
|
||||
val unstaged = if (showSearchUnstaged && filterUnstaged.text.isNotBlank()) {
|
||||
state.unstaged.filter { it.filePath.lowercaseContains(filterUnstaged.text) }
|
||||
} else {
|
||||
state.unstaged
|
||||
}
|
||||
|
||||
val staged = if (showSearchStaged && filterStaged.text.isNotBlank()) {
|
||||
state.staged.filter { it.filePath.lowercaseContains(filterStaged.text) }
|
||||
} else {
|
||||
state.staged
|
||||
}
|
||||
|
||||
state.copy(stagedFiltered = staged, unstagedFiltered = unstaged)
|
||||
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}.stateIn(
|
||||
tabScope,
|
||||
SharingStarted.Lazily,
|
||||
StageState.Loading
|
||||
)
|
||||
|
||||
var savedCommitMessage = CommitMessage("", MessageType.NORMAL)
|
||||
|
||||
@ -145,7 +186,7 @@ class StatusViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun loadStatus(git: Git) {
|
||||
val previousStatus = _stageStatus.value
|
||||
val previousStatus = _stageState.value
|
||||
|
||||
val requiredMessageType = if (git.repository.repositoryState == RepositoryState.MERGING) {
|
||||
MessageType.MERGE
|
||||
@ -166,10 +207,10 @@ class StatusViewModel @Inject constructor(
|
||||
delayedStateChange(
|
||||
delayMs = MIN_TIME_IN_MS_TO_SHOW_LOAD,
|
||||
onDelayTriggered = {
|
||||
if (previousStatus is StageStatus.Loaded) {
|
||||
_stageStatus.value = previousStatus.copy(isPartiallyReloading = true)
|
||||
if (previousStatus is StageState.Loaded) {
|
||||
_stageState.value = previousStatus.copy(isPartiallyReloading = true)
|
||||
} else {
|
||||
_stageStatus.value = StageStatus.Loading
|
||||
_stageState.value = StageState.Loading
|
||||
}
|
||||
}
|
||||
) {
|
||||
@ -177,10 +218,15 @@ class StatusViewModel @Inject constructor(
|
||||
val staged = getStagedUseCase(status).sortedBy { it.filePath }
|
||||
val unstaged = getUnstagedUseCase(status).sortedBy { it.filePath }
|
||||
|
||||
_stageStatus.value = StageStatus.Loaded(staged, unstaged, isPartiallyReloading = false)
|
||||
_stageState.value = StageState.Loaded(
|
||||
staged = staged,
|
||||
stagedFiltered = staged,
|
||||
unstaged = unstaged,
|
||||
unstagedFiltered = unstaged, isPartiallyReloading = false
|
||||
)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
_stageStatus.value = previousStatus
|
||||
_stageState.value = previousStatus
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
@ -330,15 +376,33 @@ class StatusViewModel @Inject constructor(
|
||||
fun onAcceptCommitterData(newAuthorInfo: AuthorInfo, persist: Boolean) {
|
||||
this._committerDataRequestState.value = CommitterDataRequestState.Accepted(newAuthorInfo, persist)
|
||||
}
|
||||
|
||||
fun onSearchFilterToggledStaged(visible: Boolean) {
|
||||
_showSearchStaged.value = visible
|
||||
}
|
||||
|
||||
fun onSearchFilterChangedStaged(filter: TextFieldValue) {
|
||||
_searchFilterStaged.value = filter
|
||||
}
|
||||
|
||||
fun onSearchFilterToggledUnstaged(visible: Boolean) {
|
||||
_showSearchUnstaged.value = visible
|
||||
}
|
||||
|
||||
fun onSearchFilterChangedUnstaged(filter: TextFieldValue) {
|
||||
_searchFilterUnstaged.value = filter
|
||||
}
|
||||
}
|
||||
|
||||
sealed class StageStatus {
|
||||
object Loading : StageStatus()
|
||||
sealed class StageState {
|
||||
object Loading : StageState()
|
||||
data class Loaded(
|
||||
val staged: List<StatusEntry>,
|
||||
val stagedFiltered: List<StatusEntry>,
|
||||
val unstaged: List<StatusEntry>,
|
||||
val unstagedFiltered: List<StatusEntry>,
|
||||
val isPartiallyReloading: Boolean
|
||||
) : StageStatus()
|
||||
) : StageState()
|
||||
}
|
||||
|
||||
data class CommitMessage(val message: String, val messageType: MessageType)
|
||||
|
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