Compare commits

..

1 commit

Author SHA1 Message Date
Abdelilah El Aissaoui
913b9c65e3
[WIP] Reduced heigh of multiple lists elements 2024-01-18 13:19:44 +01:00
28 changed files with 563 additions and 1607 deletions

View file

@ -8,13 +8,13 @@ crate-type = ["cdylib"]
name = "gitnuro_rs"
[dependencies]
uniffi = { version = "0.26.0" }
notify = "6.1.1"
thiserror = "1.0.56"
uniffi = { version = "0.25.0" }
notify = "6.0.1"
thiserror = "1.0.43"
libssh-rs = { version = "0.2.2", features = ["vendored", "vendored-openssl"] }
[build-dependencies]
uniffi = { version = "0.26.0", features = ["build"] }
uniffi = { version = "0.25.0", features = ["build"] }
[[bin]]
name = "uniffi-bindgen"

View file

@ -278,7 +278,7 @@ class App {
},
onTabClosed = onCloseTab,
onAddNewTab = onAddedTab,
tabsHeight = 40.dp,
tabsHeight = 36.dp,
onMoveTab = { fromIndex, toIndex ->
tabsManager.onMoveTab(fromIndex, toIndex)
},

View file

@ -25,8 +25,6 @@ object AppIcons {
const val ERROR = "error.svg"
const val EXPAND_MORE = "expand_more.svg"
const val FETCH = "fetch.svg"
const val FOLDER = "folder.svg"
const val FOLDER_OPEN = "folder_open.svg"
const val GRADE = "grade.svg"
const val HORIZONTAL_SPLIT = "horizontal_split.svg"
const val HISTORY = "history.svg"
@ -61,7 +59,6 @@ object AppIcons {
const val TAG = "tag.svg"
const val TERMINAL = "terminal.svg"
const val TOPIC = "topic.svg"
const val TREE = "tree.svg"
const val UNDO = "undo.svg"
const val UNIFIED = "unified.svg"
const val UPDATE = "update.svg"

View file

@ -1,15 +0,0 @@
package com.jetpackduba.gitnuro.git.workspace
import com.jetpackduba.gitnuro.system.systemSeparator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import javax.inject.Inject
class StageByDirectoryUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, dir: String) = withContext(Dispatchers.IO) {
git.add()
.addFilepattern(dir)
.call()
}
}

View file

@ -1,15 +0,0 @@
package com.jetpackduba.gitnuro.git.workspace
import com.jetpackduba.gitnuro.system.systemSeparator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import javax.inject.Inject
class UnstageByDirectoryUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, dir: String) = withContext(Dispatchers.IO) {
git.reset()
.addPath(dir)
.call()
}
}

View file

@ -31,7 +31,6 @@ private const val PREF_DIFF_TYPE = "diffType"
private const val PREF_DIFF_FULL_FILE = "diffFullFile"
private const val PREF_SWAP_UNCOMMITTED_CHANGES = "inverseUncommittedChanges"
private const val PREF_TERMINAL_PATH = "terminalPath"
private const val PREF_SHOW_CHANGES_AS_TREE = "showChangesAsTree"
private const val PREF_USE_PROXY = "useProxy"
private const val PREF_PROXY_TYPE = "proxyType"
private const val PREF_PROXY_HOST_NAME = "proxyHostName"
@ -51,7 +50,6 @@ private const val PREF_VERIFY_SSL = "verifySsl"
private const val DEFAULT_COMMITS_LIMIT = 1000
private const val DEFAULT_COMMITS_LIMIT_ENABLED = true
private const val DEFAULT_SWAP_UNCOMMITTED_CHANGES = false
private const val DEFAULT_SHOW_CHANGES_AS_TREE = false
private const val DEFAULT_CACHE_CREDENTIALS_IN_MEMORY = true
private const val DEFAULT_VERIFY_SSL = true
const val DEFAULT_UI_SCALE = -1f
@ -69,9 +67,6 @@ class AppSettings @Inject constructor() {
private val _swapUncommittedChangesFlow = MutableStateFlow(swapUncommittedChanges)
val swapUncommittedChangesFlow = _swapUncommittedChangesFlow.asStateFlow()
private val _showChangesAsTreeFlow = MutableStateFlow(showChangesAsTree)
val showChangesAsTreeFlow = _showChangesAsTreeFlow.asStateFlow()
private val _cacheCredentialsInMemoryFlow = MutableStateFlow(cacheCredentialsInMemory)
val cacheCredentialsInMemoryFlow = _cacheCredentialsInMemoryFlow.asStateFlow()
@ -170,15 +165,6 @@ class AppSettings @Inject constructor() {
_swapUncommittedChangesFlow.value = value
}
var showChangesAsTree: Boolean
get() {
return preferences.getBoolean(PREF_SHOW_CHANGES_AS_TREE, DEFAULT_SHOW_CHANGES_AS_TREE)
}
set(value) {
preferences.putBoolean(PREF_SHOW_CHANGES_AS_TREE, value)
_showChangesAsTreeFlow.value = value
}
var cacheCredentialsInMemory: Boolean
get() {
return preferences.getBoolean(PREF_CACHE_CREDENTIALS_IN_MEMORY, DEFAULT_CACHE_CREDENTIALS_IN_MEMORY)
@ -361,6 +347,18 @@ class AppSettings @Inject constructor() {
_customThemeFlow.value = Json.decodeFromString<ColorsScheme>(themeJson)
}
}
private fun loadProxySettings() {
_proxyFlow.value = ProxySettings(
useProxy,
proxyType,
proxyHostName,
proxyPortNumber,
proxyUseAuth,
proxyHostUser,
proxyHostPassword,
)
}
}
data class ProxySettings(

View file

@ -40,7 +40,7 @@ class OpenFilePickerUseCase @Inject constructor(
if (isZenityInstalled) {
val command = when (pickerType) {
PickerType.FILES -> listOf(
PickerType.FILES, PickerType.FILES_AND_DIRECTORIES -> listOf(
"zenity",
"--file-selection",
"--title=Open"
@ -70,21 +70,15 @@ class OpenFilePickerUseCase @Inject constructor(
}
if (isMac) {
if (pickerType == PickerType.DIRECTORIES) {
System.setProperty("apple.awt.fileDialogForDirectories", "true")
}
val fileChooser = if (basePath.isNullOrEmpty()) {
System.setProperty("apple.awt.fileDialogForDirectories", "true")
val fileChooser = if (basePath.isNullOrEmpty())
FileDialog(null as java.awt.Frame?, "Open", FileDialog.LOAD)
} else {
else
FileDialog(null as java.awt.Frame?, "Open", FileDialog.LOAD).apply {
directory = basePath
}
}
fileChooser.isMultipleMode = false
fileChooser.isVisible = true
System.setProperty("apple.awt.fileDialogForDirectories", "false")
if (fileChooser.file != null && fileChooser.directory != null) {
@ -109,5 +103,6 @@ class OpenFilePickerUseCase @Inject constructor(
enum class PickerType(val value: Int) {
FILES(JFileChooser.FILES_ONLY),
DIRECTORIES(JFileChooser.DIRECTORIES_ONLY);
DIRECTORIES(JFileChooser.DIRECTORIES_ONLY),
FILES_AND_DIRECTORIES(JFileChooser.FILES_AND_DIRECTORIES);
}

View file

@ -1,5 +1,6 @@
package com.jetpackduba.gitnuro.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -20,17 +21,18 @@ 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
import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.extensions.*
import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.theme.backgroundSelected
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
import com.jetpackduba.gitnuro.theme.tertiarySurface
import com.jetpackduba.gitnuro.ui.components.*
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.committedChangesEntriesContextMenuItems
import com.jetpackduba.gitnuro.ui.tree_files.TreeItem
import com.jetpackduba.gitnuro.viewmodels.CommitChangesStateUi
import com.jetpackduba.gitnuro.viewmodels.CommitChangesState
import com.jetpackduba.gitnuro.viewmodels.CommitChangesViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -51,29 +53,28 @@ fun CommitChanges(
commitChangesViewModel.loadChanges(selectedItem.revCommit)
}
val commitChangesStatus = commitChangesViewModel.commitChangesStateUi.collectAsState().value
val commitChangesStatus = commitChangesViewModel.commitChangesState.collectAsState().value
val showSearch by commitChangesViewModel.showSearch.collectAsState()
val changesListScroll by commitChangesViewModel.changesLazyListState.collectAsState()
val textScroll by commitChangesViewModel.textScroll.collectAsState()
val showAsTree by commitChangesViewModel.showAsTree.collectAsState()
var searchFilter by remember(commitChangesViewModel, showSearch, commitChangesStatus) {
mutableStateOf(commitChangesViewModel.searchFilter.value)
}
when (commitChangesStatus) {
CommitChangesStateUi.Loading -> {
CommitChangesState.Loading -> {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant)
}
is CommitChangesStateUi.Loaded -> {
is CommitChangesState.Loaded -> {
CommitChangesView(
diffSelected = diffSelected,
commitChangesStatus = commitChangesStatus,
commit = commitChangesStatus.commit,
changes = commitChangesStatus.changesFiltered,
onBlame = onBlame,
onHistory = onHistory,
showSearch = showSearch,
showAsTree = showAsTree,
changesListScroll = changesListScroll,
textScroll = textScroll,
searchFilter = searchFilter,
@ -85,37 +86,38 @@ fun CommitChanges(
searchFilter = filter
commitChangesViewModel.onSearchFilterChanged(filter)
},
onDirectoryClicked = { commitChangesViewModel.onDirectoryClicked(it.fullPath) },
onAlternateShowAsTree = { commitChangesViewModel.alternateShowAsTree() },
)
}
}
}
@Composable
private fun CommitChangesView(
commitChangesStatus: CommitChangesStateUi.Loaded,
fun CommitChangesView(
commit: RevCommit,
changes: List<DiffEntry>,
diffSelected: DiffEntryType?,
changesListScroll: LazyListState,
textScroll: ScrollState,
showSearch: Boolean,
showAsTree: Boolean,
searchFilter: TextFieldValue,
onBlame: (String) -> Unit,
onHistory: (String) -> Unit,
onDiffSelected: (DiffEntry) -> Unit,
onSearchFilterToggled: (Boolean) -> Unit,
onSearchFilterChanged: (TextFieldValue) -> Unit,
onDirectoryClicked: (TreeItem.Dir) -> Unit,
onAlternateShowAsTree: () -> Unit,
) {
val commit = commitChangesStatus.commit
/**
* 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 searchFocusRequester = remember { FocusRequester() }
Column(
modifier = Modifier
@ -124,168 +126,91 @@ private fun CommitChangesView(
.weight(1f, fill = true)
.background(MaterialTheme.colors.background)
) {
Header(
showSearch,
searchFilter,
onSearchFilterChanged,
onSearchFilterToggled,
showAsTree = showAsTree,
onAlternateShowAsTree = onAlternateShowAsTree,
)
Row(
modifier = Modifier
.fillMaxWidth()
.height(34.dp)
.background(MaterialTheme.colors.tertiarySurface),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 16.dp),
text = "Files changed",
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Left,
color = MaterialTheme.colors.onBackground,
maxLines = 1,
style = MaterialTheme.typography.body2,
)
when (commitChangesStatus) {
is CommitChangesStateUi.ListLoaded -> {
val changes = commitChangesStatus.changes
Box(modifier = Modifier.weight(1f))
ListCommitLogChanges(
diffSelected = diffSelected,
changesListScroll = changesListScroll,
diffEntries = changes,
onDiffSelected = onDiffSelected,
onGenerateContextMenu = { diffEntry ->
committedChangesEntriesContextMenuItems(
diffEntry,
onBlame = { onBlame(diffEntry.filePath) },
onHistory = { onHistory(diffEntry.filePath) },
)
}
)
}
IconButton(
onClick = {
onSearchFilterToggled(!showSearch)
is CommitChangesStateUi.TreeLoaded -> {
TreeCommitLogChanges(
diffSelected = diffSelected,
changesListScroll = changesListScroll,
treeItems = commitChangesStatus.changes,
onDiffSelected = onDiffSelected,
onGenerateContextMenu = { diffEntry ->
committedChangesEntriesContextMenuItems(
diffEntry,
onBlame = { onBlame(diffEntry.filePath) },
onHistory = { onHistory(diffEntry.filePath) },
)
},
onDirectoryClicked = onDirectoryClicked,
if (!showSearch)
requestFocus = true
},
modifier = Modifier.handOnHover(),
) {
Icon(
painter = painterResource(AppIcons.SEARCH),
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colors.onBackground,
)
}
}
if (showSearch) {
SearchTextField(
searchFilter = searchFilter,
onSearchFilterChanged = onSearchFilterChanged,
searchFocusRequester = searchFocusRequester,
onClose = { onSearchFilterToggled(false) },
)
}
LaunchedEffect(showSearch, requestFocus) {
if (showSearch && requestFocus) {
searchFocusRequester.requestFocus()
requestFocus = false
}
}
CommitLogChanges(
diffSelected = diffSelected,
changesListScroll = changesListScroll,
diffEntries = changes,
onDiffSelected = onDiffSelected,
onBlame = onBlame,
onHistory = onHistory,
)
}
MessageAuthorFooter(commit, textScroll)
}
}
@Composable
private fun Header(
showSearch: Boolean,
searchFilter: TextFieldValue,
onSearchFilterChanged: (TextFieldValue) -> Unit,
onSearchFilterToggled: (Boolean) -> Unit,
showAsTree: Boolean,
onAlternateShowAsTree: () -> Unit,
) {
val searchFocusRequester = remember { FocusRequester() }
/**
* State used to prevent the text field from getting the focus when returning from another tab
*/
var requestFocus by remember { mutableStateOf(false) }
Row(
modifier = Modifier
.fillMaxWidth()
.height(34.dp)
.background(MaterialTheme.colors.tertiarySurface),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
Column(
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 16.dp),
text = "Files changed",
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Left,
color = MaterialTheme.colors.onBackground,
maxLines = 1,
style = MaterialTheme.typography.body2,
)
Box(modifier = Modifier.weight(1f))
IconButton(
onClick = {
onAlternateShowAsTree()
},
modifier = Modifier.handOnHover()
.fillMaxWidth()
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colors.background),
) {
Icon(
painter = painterResource(if (showAsTree) AppIcons.LIST else AppIcons.TREE),
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colors.onBackground,
)
SelectionContainer {
Text(
text = commit.fullMessage,
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground,
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.padding(8.dp)
.verticalScroll(textScroll),
)
}
Author(commit.shortName, commit.name, commit.authorIdent)
}
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,
)
}
}
if (showSearch) {
SearchTextField(
searchFilter = searchFilter,
onSearchFilterChanged = onSearchFilterChanged,
searchFocusRequester = searchFocusRequester,
onClose = { onSearchFilterToggled(false) },
)
}
LaunchedEffect(showSearch, requestFocus) {
if (showSearch && requestFocus) {
searchFocusRequester.requestFocus()
requestFocus = false
}
}
}
@Composable
private fun MessageAuthorFooter(
commit: RevCommit,
textScroll: ScrollState,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colors.background),
) {
SelectionContainer {
Text(
text = commit.fullMessage,
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground,
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.padding(8.dp)
.verticalScroll(textScroll),
)
}
Author(commit.shortName, commit.name, commit.authorIdent)
}
}
@ -375,13 +300,15 @@ fun Author(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListCommitLogChanges(
fun CommitLogChanges(
diffEntries: List<DiffEntry>,
diffSelected: DiffEntryType?,
changesListScroll: LazyListState,
onBlame: (String) -> Unit,
onHistory: (String) -> Unit,
onDiffSelected: (DiffEntry) -> Unit,
onGenerateContextMenu: (DiffEntry) -> List<ContextMenuElement>,
) {
ScrollableLazyColumn(
modifier = Modifier
@ -389,96 +316,74 @@ fun ListCommitLogChanges(
state = changesListScroll,
) {
items(items = diffEntries) { diffEntry ->
FileEntry(
icon = diffEntry.icon,
iconColor = diffEntry.iconColor,
parentDirectoryPath = diffEntry.parentDirectoryPath,
fileName = diffEntry.fileName,
isSelected = diffSelected is DiffEntryType.CommitDiff && diffSelected.diffEntry == diffEntry,
onClick = { onDiffSelected(diffEntry) },
onDoubleClick = {},
onGenerateContextMenu = { onGenerateContextMenu(diffEntry) },
trailingAction = null,
)
ContextMenu(
items = {
committedChangesEntriesContextMenuItems(
diffEntry,
onBlame = { onBlame(diffEntry.filePath) },
onHistory = { onHistory(diffEntry.filePath) },
)
}
) {
Column(
modifier = Modifier
.height(36.dp)
.fillMaxWidth()
.handMouseClickable {
onDiffSelected(diffEntry)
}
.backgroundIf(
condition = diffSelected is DiffEntryType.CommitDiff && diffSelected.diffEntry == diffEntry,
color = MaterialTheme.colors.backgroundSelected,
),
verticalArrangement = Arrangement.Center,
) {
Spacer(modifier = Modifier.weight(2f))
Row {
Icon(
modifier = Modifier
.padding(horizontal = 8.dp)
.size(16.dp),
imageVector = diffEntry.icon,
contentDescription = null,
tint = diffEntry.iconColor,
)
if (diffEntry.parentDirectoryPath.isNotEmpty()) {
Text(
text = diffEntry.parentDirectoryPath.removeSuffix("/"),
modifier = Modifier.weight(1f, fill = false),
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackgroundSecondary,
)
Text(
text = "/",
maxLines = 1,
softWrap = false,
style = MaterialTheme.typography.body2,
overflow = TextOverflow.Visible,
color = MaterialTheme.colors.onBackgroundSecondary,
)
}
Text(
text = diffEntry.fileName,
maxLines = 1,
softWrap = false,
modifier = Modifier.padding(end = 16.dp),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackground,
)
}
Spacer(modifier = Modifier.weight(2f))
}
}
}
}
}
@Composable
fun TreeCommitLogChanges(
treeItems: List<TreeItem<DiffEntry>>,
diffSelected: DiffEntryType?,
changesListScroll: LazyListState,
onDiffSelected: (DiffEntry) -> Unit,
onDirectoryClicked: (TreeItem.Dir) -> Unit,
onGenerateContextMenu: (DiffEntry) -> List<ContextMenuElement>,
) {
ScrollableLazyColumn(
modifier = Modifier
.fillMaxSize(),
state = changesListScroll,
) {
items(items = treeItems) { entry ->
CommitTreeItemEntry(
entry = entry,
isSelected = entry is TreeItem.File &&
diffSelected is DiffEntryType.CommitDiff &&
diffSelected.diffEntry == entry.data,
onFileClick = { onDiffSelected(it) },
onDirectoryClick = { onDirectoryClicked(it) },
onGenerateContextMenu = onGenerateContextMenu,
onGenerateDirectoryContextMenu = { emptyList() },
)
}
}
}
@Composable
private fun CommitTreeItemEntry(
entry: TreeItem<DiffEntry>,
isSelected: Boolean,
onFileClick: (DiffEntry) -> Unit,
onDirectoryClick: (TreeItem.Dir) -> Unit,
onGenerateContextMenu: (DiffEntry) -> List<ContextMenuElement>,
onGenerateDirectoryContextMenu: (TreeItem.Dir) -> List<ContextMenuElement>,
) {
when (entry) {
is TreeItem.File -> CommitFileEntry(
fileEntry = entry,
isSelected = isSelected,
onClick = { onFileClick(entry.data) },
onGenerateContextMenu = onGenerateContextMenu,
)
is TreeItem.Dir -> DirectoryEntry(
dirName = entry.displayName,
onClick = { onDirectoryClick(entry) },
depth = entry.depth,
onGenerateContextMenu = { onGenerateDirectoryContextMenu(entry) },
)
}
}
@Composable
private fun CommitFileEntry(
fileEntry: TreeItem.File<DiffEntry>,
isSelected: Boolean,
onClick: () -> Unit,
onGenerateContextMenu: (DiffEntry) -> List<ContextMenuElement>,
) {
val diffEntry = fileEntry.data
FileEntry(
icon = diffEntry.icon,
iconColor = diffEntry.iconColor,
parentDirectoryPath = "",
fileName = diffEntry.fileName,
isSelected = isSelected,
onClick = onClick,
onDoubleClick = {},
depth = fileEntry.depth,
onGenerateContextMenu = { onGenerateContextMenu(diffEntry) },
trailingAction = null,
)
}

View file

@ -28,7 +28,7 @@ import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.extensions.handOnHover
import com.jetpackduba.gitnuro.extensions.ignoreKeyEvents
import com.jetpackduba.gitnuro.git.remote_operations.PullType
import com.jetpackduba.gitnuro.ui.components.tooltip.InstantTooltip
import com.jetpackduba.gitnuro.ui.components.InstantTooltip
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.context_menu.*
import com.jetpackduba.gitnuro.viewmodels.MenuViewModel

View file

@ -19,7 +19,6 @@ import com.jetpackduba.gitnuro.extensions.isValid
import com.jetpackduba.gitnuro.extensions.simpleName
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
import com.jetpackduba.gitnuro.ui.components.*
import com.jetpackduba.gitnuro.ui.components.tooltip.DelayedTooltip
import com.jetpackduba.gitnuro.ui.context_menu.*
import com.jetpackduba.gitnuro.ui.dialogs.AddSubmodulesDialog
import com.jetpackduba.gitnuro.ui.dialogs.EditRemotesDialog
@ -438,7 +437,6 @@ private fun Branch(
) {
SideMenuSubentry(
text = branch.simpleName,
fontWeight = if (isCurrentBranch) FontWeight.Bold else FontWeight.Normal,
iconResourcePath = AppIcons.BRANCH,
isSelected = isSelectedItem,
onClick = onBranchClicked,
@ -598,7 +596,7 @@ private fun Submodule(
},
) {
val stateName = submodule.second.type.toString()
DelayedTooltip(stateName) {
Tooltip(stateName) {
Text(
text = stateName.first().toString(),
color = MaterialTheme.colors.onBackgroundSecondary,

File diff suppressed because it is too large Load diff

View file

@ -1,161 +0,0 @@
package com.jetpackduba.gitnuro.ui.components
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.extensions.backgroundIf
import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.extensions.onDoubleClick
import com.jetpackduba.gitnuro.theme.backgroundSelected
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement
private const val TREE_START_PADDING = 12
@Composable
fun FileEntry(
icon: ImageVector,
iconColor: Color,
parentDirectoryPath: String,
fileName: String,
isSelected: Boolean,
onClick: () -> Unit,
onDoubleClick: () -> Unit,
depth: Int = 0,
onGenerateContextMenu: () -> List<ContextMenuElement>,
trailingAction: (@Composable BoxScope.(isHovered: Boolean) -> Unit)?,
) {
FileEntry(
icon = rememberVectorPainter(icon),
iconColor = iconColor,
parentDirectoryPath = parentDirectoryPath,
fileName = fileName,
isSelected = isSelected,
onClick = onClick,
onDoubleClick = onDoubleClick,
depth = depth,
onGenerateContextMenu = onGenerateContextMenu,
trailingAction = trailingAction
)
}
@Composable
fun FileEntry(
icon: Painter,
iconColor: Color,
parentDirectoryPath: String,
fileName: String,
isSelected: Boolean,
onClick: () -> Unit,
onDoubleClick: () -> Unit,
depth: Int = 0,
onGenerateContextMenu: () -> List<ContextMenuElement>,
trailingAction: (@Composable BoxScope.(isHovered: Boolean) -> Unit)?,
) {
val hoverInteraction = remember { MutableInteractionSource() }
val isHovered by hoverInteraction.collectIsHoveredAsState()
Box(
modifier = Modifier
.handMouseClickable { onClick() }
.onDoubleClick(onDoubleClick)
.fillMaxWidth()
.hoverable(hoverInteraction)
) {
ContextMenu(
items = {
onGenerateContextMenu()
},
) {
Row(
modifier = Modifier
.height(40.dp)
.fillMaxWidth()
.backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected)
.padding(start = (TREE_START_PADDING * depth).dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = icon,
contentDescription = null,
modifier = Modifier
.padding(horizontal = 8.dp)
.size(16.dp),
tint = iconColor,
)
if (parentDirectoryPath.isNotEmpty()) {
Text(
text = parentDirectoryPath.removeSuffix("/"),
modifier = Modifier.weight(1f, fill = false),
maxLines = 1,
softWrap = false,
style = MaterialTheme.typography.body2,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colors.onBackgroundSecondary,
)
Text(
text = "/",
maxLines = 1,
softWrap = false,
style = MaterialTheme.typography.body2,
overflow = TextOverflow.Visible,
color = MaterialTheme.colors.onBackgroundSecondary,
)
}
Text(
text = fileName,
maxLines = 1,
softWrap = false,
modifier = Modifier.padding(end = 16.dp),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackground,
)
}
}
trailingAction?.invoke(this, isHovered)
}
}
@Composable
fun DirectoryEntry(
dirName: String,
onClick: () -> Unit,
depth: Int = 0,
onGenerateContextMenu: () -> List<ContextMenuElement>,
) {
FileEntry(
icon = painterResource(AppIcons.FOLDER),
iconColor = MaterialTheme.colors.onBackground,
isSelected = false,
onClick = onClick,
onDoubleClick = {},
parentDirectoryPath = "",
fileName = dirName,
depth = depth,
onGenerateContextMenu = onGenerateContextMenu,
trailingAction = null,
)
}

View file

@ -1,4 +1,4 @@
package com.jetpackduba.gitnuro.ui.components.tooltip
package com.jetpackduba.gitnuro.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -6,6 +6,7 @@ import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -103,6 +104,7 @@ fun InstantTooltip(
modifier = Modifier.padding(8.dp)
)
}
}
}
}

View file

@ -31,8 +31,6 @@ import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.extensions.handOnHover
import com.jetpackduba.gitnuro.extensions.onMiddleMouseButtonClick
import com.jetpackduba.gitnuro.managers.AppStateManager
import com.jetpackduba.gitnuro.ui.components.tooltip.DelayedTooltip
import com.jetpackduba.gitnuro.ui.components.tooltip.InstantTooltip
import com.jetpackduba.gitnuro.ui.drag_sorting.HorizontalDraggableItem
import com.jetpackduba.gitnuro.ui.drag_sorting.horizontalDragContainer
import com.jetpackduba.gitnuro.ui.drag_sorting.rememberHorizontalDragDropState
@ -82,7 +80,7 @@ fun RepositoriesTabPanel(
Column {
if (canBeScrolled) {
DelayedTooltip(
Tooltip(
"\"Shift + Mouse wheel\" to scroll"
) {
HorizontalScrollbar(

View file

@ -13,7 +13,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.extensions.backgroundIf
@ -27,7 +26,6 @@ const val ENTRY_HEIGHT = 36
@Composable
fun SideMenuSubentry(
text: String,
fontWeight: FontWeight = FontWeight.Normal,
iconResourcePath: String,
isSelected: Boolean,
extraPadding: Dp = 0.dp,
@ -61,7 +59,6 @@ fun SideMenuSubentry(
Text(
text = text,
fontWeight = fontWeight,
modifier = Modifier.weight(1f, fill = true),
maxLines = 1,
style = MaterialTheme.typography.body2,

View file

@ -1,4 +1,4 @@
package com.jetpackduba.gitnuro.ui.components.tooltip
package com.jetpackduba.gitnuro.ui.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
@ -15,7 +15,7 @@ import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DelayedTooltip(text: String?, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
fun Tooltip(text: String?, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
TooltipArea(
modifier = modifier,
tooltip = {

View file

@ -15,7 +15,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import com.jetpackduba.gitnuro.ui.components.tooltip.DelayedTooltip
@OptIn(ExperimentalFoundationApi::class)
@Composable
@ -38,7 +37,7 @@ fun TooltipText(
style: TextStyle = LocalTextStyle.current,
tooltipTitle: String,
) {
DelayedTooltip(
Tooltip(
text = tooltipTitle,
) {
Text(

View file

@ -220,7 +220,7 @@ fun showPopup(x: Int, y: Int, contextMenuElements: List<ContextMenuElement>, onD
fun Separator() {
Box(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.padding(horizontal = 16.dp)
.fillMaxWidth()
.height(1.dp)
.background(MaterialTheme.colors.onBackground.copy(alpha = 0.4f))

View file

@ -1,40 +0,0 @@
package com.jetpackduba.gitnuro.ui.context_menu
import androidx.compose.ui.res.painterResource
import com.jetpackduba.gitnuro.AppIcons
fun statusDirEntriesContextMenuItems(
entryType: EntryType,
onStageChanges: () -> Unit,
onDiscardDirectoryChanges: () -> Unit,
): List<ContextMenuElement> {
return mutableListOf<ContextMenuElement>().apply {
val (text, icon) = if (entryType == EntryType.STAGED) {
"Unstage changes in the directory" to AppIcons.REMOVE_DONE
} else {
"Stage changes in the directory" to AppIcons.DONE
}
add(
ContextMenuElement.ContextTextEntry(
label = text,
icon = { painterResource(icon) },
onClick = onStageChanges,
)
)
if (entryType == EntryType.UNSTAGED) {
add(ContextMenuElement.ContextSeparator)
add(
ContextMenuElement.ContextTextEntry(
label = "Discard changes in the directory",
icon = { painterResource(AppIcons.UNDO) },
onClick = onDiscardDirectoryChanges,
)
)
}
}
}

View file

@ -1,35 +1,25 @@
package com.jetpackduba.gitnuro.ui.dialogs
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ClipboardManager
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.unit.dp
import androidx.compose.ui.unit.sp
import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.extensions.handOnHover
import com.jetpackduba.gitnuro.managers.Error
import com.jetpackduba.gitnuro.theme.secondarySurface
import com.jetpackduba.gitnuro.ui.components.PrimaryButton
import com.jetpackduba.gitnuro.ui.components.tooltip.InstantTooltip
import kotlinx.coroutines.delay
@Composable
fun ErrorDialog(
error: Error,
onAccept: () -> Unit,
) {
val horizontalScroll = rememberScrollState()
val verticalScroll = rememberScrollState()
val clipboard = LocalClipboardManager.current
MaterialDialog {
Column(
modifier = Modifier
@ -62,61 +52,6 @@ fun ErrorDialog(
style = MaterialTheme.typography.body2,
)
Box(
modifier = Modifier
.padding(top = 24.dp)
.height(400.dp)
.fillMaxWidth()
) {
OutlinedTextField(
value = error.exception.stackTraceToString(),
onValueChange = {},
readOnly = true,
colors = TextFieldDefaults.outlinedTextFieldColors(backgroundColor = MaterialTheme.colors.secondarySurface),
textStyle = MaterialTheme.typography.body2,
modifier = Modifier
.fillMaxSize()
.horizontalScroll(horizontalScroll)
.verticalScroll(verticalScroll),
)
HorizontalScrollbar(
rememberScrollbarAdapter(horizontalScroll),
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
)
VerticalScrollbar(
rememberScrollbarAdapter(verticalScroll),
modifier = Modifier
.align(Alignment.CenterEnd)
.fillMaxHeight()
)
InstantTooltip(
"Copy error",
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 16.dp, bottom = 16.dp)
) {
IconButton(
onClick = {
copyMessageError(clipboard, error.exception)
},
modifier = Modifier
.size(24.dp)
.handOnHover()
) {
Icon(
painter = painterResource(AppIcons.COPY),
contentDescription = "Copy stacktrace",
tint = MaterialTheme.colors.onSurface,
)
}
}
}
Row(
modifier = Modifier
.align(Alignment.End)
@ -129,8 +64,4 @@ fun ErrorDialog(
}
}
}
}
fun copyMessageError(clipboard: ClipboardManager, ex: Exception) {
clipboard.setText(AnnotatedString(ex.stackTraceToString()))
}
}

View file

@ -57,7 +57,7 @@ import com.jetpackduba.gitnuro.theme.*
import com.jetpackduba.gitnuro.ui.components.PrimaryButton
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
import com.jetpackduba.gitnuro.ui.components.SecondaryButton
import com.jetpackduba.gitnuro.ui.components.tooltip.DelayedTooltip
import com.jetpackduba.gitnuro.ui.components.Tooltip
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement
import com.jetpackduba.gitnuro.ui.context_menu.CustomTextContextMenu
@ -945,7 +945,7 @@ fun StateIcon(
isToggled: Boolean,
onClick: () -> Unit,
) {
DelayedTooltip(tooltip) {
Tooltip(tooltip) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState
@ -30,8 +29,6 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
@ -48,12 +45,7 @@ import com.jetpackduba.gitnuro.keybindings.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.theme.*
import com.jetpackduba.gitnuro.ui.SelectedItem
import com.jetpackduba.gitnuro.ui.components.AvatarImage
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn
import com.jetpackduba.gitnuro.ui.components.gitnuroDynamicViewModel
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.components.tooltip.InstantTooltip
import com.jetpackduba.gitnuro.ui.components.tooltip.InstantTooltipPosition
import com.jetpackduba.gitnuro.ui.components.*
import com.jetpackduba.gitnuro.ui.context_menu.*
import com.jetpackduba.gitnuro.ui.dialogs.NewBranchDialog
import com.jetpackduba.gitnuro.ui.dialogs.NewTagDialog
@ -81,8 +73,6 @@ private const val CANVAS_MIN_WIDTH = 100
private const val CANVAS_DEFAULT_WIDTH = 120
private const val MIN_GRAPH_LANES = 2
private const val HORIZONTAL_SCROLL_PIXELS_MULTIPLIER = 10
/**
* Additional number of lanes to simulate to create a margin at the end of the graph.
*/
@ -90,7 +80,7 @@ private const val MARGIN_GRAPH_LANES = 2
private const val LANE_WIDTH = 30f
private const val DIVIDER_WIDTH = 8
private const val LINE_HEIGHT = 40
private const val LINE_HEIGHT = 36
private const val LOG_BOTTOM_PADDING = 80
// TODO Min size for message column
@ -433,7 +423,6 @@ fun SearchFilter(
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CommitsList(
scrollState: LazyListState,
@ -452,22 +441,9 @@ fun CommitsList(
graphWidth: Dp,
horizontalScrollState: ScrollState,
) {
val scope = rememberCoroutineScope()
ScrollableLazyColumn(
state = scrollState,
modifier = Modifier
.fillMaxSize()
// The underlying composable assigned to the horizontal scroll bar won't be receiving the scroll events
// because the commits list will consume the events, so this code tries to scroll manually when it detects
// horizontal scrolling
.onPointerEvent(PointerEventType.Scroll) { pointerEvent ->
scope.launch {
val xScroll = pointerEvent.changes.map { it.scrollDelta.x }.sum()
horizontalScrollState.scrollBy(xScroll * HORIZONTAL_SCROLL_PIXELS_MULTIPLIER)
}
println(pointerEvent)
},
modifier = Modifier.fillMaxSize(),
) {
if (
hasUncommittedChanges ||
@ -659,7 +635,7 @@ fun UncommittedChangesLine(
) {
Row(
modifier = Modifier
.height(40.dp)
.height(LINE_HEIGHT.dp)
.padding(start = graphWidth)
.backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected)
.padding(DIVIDER_WIDTH.dp),

View file

@ -1,70 +0,0 @@
package com.jetpackduba.gitnuro.ui.tree_files
import com.jetpackduba.gitnuro.system.systemSeparator
fun <T> entriesToTreeEntry(
entries: List<T>,
treeContractedDirs: List<String>,
onGetEntryPath: (T) -> String,
): List<TreeItem<T>> {
return entries
.asSequence()
.map { entry ->
val filePath = onGetEntryPath(entry)
val parts = filePath.split(systemSeparator)
parts.mapIndexed { index, partName ->
if (index == parts.lastIndex) {
val isParentContracted = treeContractedDirs.none { contractedDir ->
filePath.startsWith(contractedDir + systemSeparator)
}
if (isParentContracted) {
TreeItem.File(entry, partName, filePath, index)
} else {
null
}
} else {
val dirPath = parts.slice(0..index).joinToString(systemSeparator)
val isParentDirectoryContracted = treeContractedDirs.any { contractedDir ->
dirPath.startsWith(contractedDir + systemSeparator) &&
dirPath != contractedDir
}
val isExactDirectoryContracted = treeContractedDirs.any { contractedDir ->
dirPath == contractedDir
}
when {
isParentDirectoryContracted -> null
isExactDirectoryContracted -> TreeItem.Dir(false, partName, dirPath, index)
else -> TreeItem.Dir(true, partName, dirPath, index)
}
}
}
}
.flatten()
.filterNotNull()
.distinct()
.sortedBy { it.fullPath }
.toList()
}
sealed interface TreeItem<out T> {
val fullPath: String
val displayName: String
val depth: Int
data class Dir(
val isExpanded: Boolean,
override val displayName: String,
override val fullPath: String,
override val depth: Int
) : TreeItem<Nothing>
data class File<T>(
val data: T,
override val displayName: String,
override val fullPath: String,
override val depth: Int
) : TreeItem<T>
}

View file

@ -10,9 +10,6 @@ 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 com.jetpackduba.gitnuro.preferences.AppSettings
import com.jetpackduba.gitnuro.ui.tree_files.TreeItem
import com.jetpackduba.gitnuro.ui.tree_files.entriesToTreeEntry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import org.eclipse.jgit.diff.DiffEntry
@ -24,7 +21,6 @@ private const val MIN_TIME_IN_MS_TO_SHOW_LOAD = 300L
class CommitChangesViewModel @Inject constructor(
private val tabState: TabState,
private val getCommitDiffEntriesUseCase: GetCommitDiffEntriesUseCase,
private val appSettings: AppSettings,
tabScope: CoroutineScope,
) {
private val _showSearch = MutableStateFlow(false)
@ -37,48 +33,26 @@ class CommitChangesViewModel @Inject constructor(
LazyListState(firstVisibleItemIndex = 0, firstVisibleItemScrollOffset = 0)
)
val textScroll = MutableStateFlow(ScrollState(0))
val showAsTree = appSettings.showChangesAsTreeFlow
private val treeContractedDirectories = MutableStateFlow(emptyList<String>())
val textScroll = MutableStateFlow(
ScrollState(0)
)
private val _commitChangesState = MutableStateFlow<CommitChangesState>(CommitChangesState.Loading)
private val commitChangesFiltered =
val commitChangesState: StateFlow<CommitChangesState> =
combine(_commitChangesState, _showSearch, _searchFilter) { state, showSearch, filter ->
if (state is CommitChangesState.Loaded && showSearch && filter.text.isNotBlank()) {
state.copy(changes = state.changes.filter { it.filePath.lowercaseContains(filter.text) })
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
}
}
val commitChangesStateUi: StateFlow<CommitChangesStateUi> = combine(
commitChangesFiltered,
showAsTree,
treeContractedDirectories,
) { commitState, showAsTree, contractedDirs ->
when (commitState) {
CommitChangesState.Loading -> CommitChangesStateUi.Loading
is CommitChangesState.Loaded -> {
if (showAsTree) {
CommitChangesStateUi.TreeLoaded(
commit = commitState.commit,
changes = entriesToTreeEntry(commitState.changes, contractedDirs) { it.filePath }
)
} else {
CommitChangesStateUi.ListLoaded(
commit = commitState.commit,
changes = commitState.changes
)
}
}
}
}
.stateIn(
}.stateIn(
tabScope,
SharingStarted.Lazily,
CommitChangesStateUi.Loading
CommitChangesState.Loading
)
@ -118,7 +92,7 @@ class CommitChangesViewModel @Inject constructor(
}
}
_commitChangesState.value = CommitChangesState.Loaded(commit, changes)
_commitChangesState.value = CommitChangesState.Loaded(commit, changes, changes)
}
}
@ -132,20 +106,6 @@ class CommitChangesViewModel @Inject constructor(
}
}
fun alternateShowAsTree() {
appSettings.showChangesAsTree = !appSettings.showChangesAsTree
}
fun onDirectoryClicked(directoryPath: String) {
val contractedDirectories = treeContractedDirectories.value
if (contractedDirectories.contains(directoryPath)) {
treeContractedDirectories.value -= directoryPath
} else {
treeContractedDirectories.value += directoryPath
}
}
fun onSearchFilterToggled(visible: Boolean) {
_showSearch.value = visible
}
@ -155,22 +115,9 @@ class CommitChangesViewModel @Inject constructor(
}
}
private sealed interface CommitChangesState {
sealed interface CommitChangesState {
data object Loading : CommitChangesState
data class Loaded(val commit: RevCommit, val changes: List<DiffEntry>) :
data class Loaded(val commit: RevCommit, val changes: List<DiffEntry>, val changesFiltered: List<DiffEntry>) :
CommitChangesState
}
sealed interface CommitChangesStateUi {
data object Loading : CommitChangesStateUi
sealed interface Loaded : CommitChangesStateUi {
val commit: RevCommit
}
data class ListLoaded(override val commit: RevCommit, val changes: List<DiffEntry>) :
Loaded
data class TreeLoaded(override val commit: RevCommit, val changes: List<TreeItem<DiffEntry>>) :
Loaded
}

View file

@ -21,8 +21,6 @@ import com.jetpackduba.gitnuro.git.repository.ResetRepositoryStateUseCase
import com.jetpackduba.gitnuro.git.workspace.*
import com.jetpackduba.gitnuro.models.AuthorInfo
import com.jetpackduba.gitnuro.preferences.AppSettings
import com.jetpackduba.gitnuro.ui.tree_files.TreeItem
import com.jetpackduba.gitnuro.ui.tree_files.entriesToTreeEntry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
@ -40,8 +38,6 @@ class StatusViewModel @Inject constructor(
private val tabState: TabState,
private val stageEntryUseCase: StageEntryUseCase,
private val unstageEntryUseCase: UnstageEntryUseCase,
private val stageByDirectoryUseCase: StageByDirectoryUseCase,
private val unstageByDirectoryUseCase: UnstageByDirectoryUseCase,
private val resetEntryUseCase: DiscardEntryUseCase,
private val stageAllUseCase: StageAllUseCase,
private val unstageAllUseCase: UnstageAllUseCase,
@ -59,8 +55,8 @@ class StatusViewModel @Inject constructor(
private val saveAuthorUseCase: SaveAuthorUseCase,
private val sharedRepositoryStateManager: SharedRepositoryStateManager,
private val getSpecificCommitMessageUseCase: GetSpecificCommitMessageUseCase,
private val appSettings: AppSettings,
tabScope: CoroutineScope,
appSettings: AppSettings,
) {
private val _showSearchUnstaged = MutableStateFlow(false)
val showSearchUnstaged: StateFlow<Boolean> = _showSearchUnstaged
@ -77,11 +73,9 @@ class StatusViewModel @Inject constructor(
val swapUncommittedChanges = appSettings.swapUncommittedChangesFlow
val rebaseInteractiveState = sharedRepositoryStateManager.rebaseInteractiveState
private val treeContractedDirectories = MutableStateFlow(emptyList<String>())
private val showAsTree = appSettings.showChangesAsTreeFlow
private val _stageState = MutableStateFlow<StageState>(StageState.Loading)
private val stageStateFiltered: StateFlow<StageState> = combine(
val stageState: StateFlow<StageState> = combine(
_stageState,
_showSearchStaged,
_searchFilterStaged,
@ -89,6 +83,7 @@ class StatusViewModel @Inject constructor(
_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 {
@ -101,49 +96,31 @@ class StatusViewModel @Inject constructor(
state.staged
}.prioritizeConflicts()
state.copy(staged = staged, unstaged = unstaged)
state.copy(stagedFiltered = staged, unstagedFiltered = unstaged)
} else {
state
}
}
.stateIn(
tabScope,
SharingStarted.Lazily,
StageState.Loading
)
}.stateIn(
tabScope,
SharingStarted.Lazily,
StageState.Loading
)
val stageStateUi: StateFlow<StageStateUi> = combine(
stageStateFiltered,
showAsTree,
treeContractedDirectories,
) { stageStateFiltered, showAsTree, contractedDirectories ->
when (stageStateFiltered) {
is StageState.Loaded -> {
if (showAsTree) {
StageStateUi.TreeLoaded(
staged = entriesToTreeEntry(stageStateFiltered.staged, contractedDirectories) { it.filePath },
unstaged = entriesToTreeEntry(stageStateFiltered.unstaged, contractedDirectories) { it.filePath },
isPartiallyReloading = stageStateFiltered.isPartiallyReloading,
)
fun List<StatusEntry>.prioritizeConflicts(): List<StatusEntry> {
return this.groupBy { it.filePath }
.map {
val statusEntries = it.value
return@map if (statusEntries.count() == 1) {
statusEntries.first()
} else {
StageStateUi.ListLoaded(
staged = stageStateFiltered.staged,
unstaged = stageStateFiltered.unstaged,
isPartiallyReloading = stageStateFiltered.isPartiallyReloading,
)
val conflictingEntry =
statusEntries.firstOrNull { entry -> entry.statusType == StatusType.CONFLICTING }
conflictingEntry ?: statusEntries.first()
}
}
StageState.Loading -> StageStateUi.Loading
}
}
.stateIn(
tabScope,
SharingStarted.Lazily,
StageStateUi.Loading
)
var savedCommitMessage = CommitMessage("", MessageType.NORMAL)
@ -271,8 +248,9 @@ class StatusViewModel @Inject constructor(
_stageState.value = StageState.Loaded(
staged = staged,
stagedFiltered = staged,
unstaged = unstaged,
isPartiallyReloading = false,
unstagedFiltered = unstaged, isPartiallyReloading = false
)
}
} catch (ex: Exception) {
@ -281,21 +259,6 @@ class StatusViewModel @Inject constructor(
}
}
private fun List<StatusEntry>.prioritizeConflicts(): List<StatusEntry> {
return this.groupBy { it.filePath }
.map {
val statusEntries = it.value
return@map if (statusEntries.count() == 1) {
statusEntries.first()
} else {
val conflictingEntry =
statusEntries.firstOrNull { entry -> entry.statusType == StatusType.CONFLICTING }
conflictingEntry ?: statusEntries.first()
}
}
}
private fun messageByRepoState(git: Git): String {
val message: String? = if (
git.repository.repositoryState.isMerging ||
@ -486,91 +449,19 @@ class StatusViewModel @Inject constructor(
fun onSearchFilterChangedUnstaged(filter: TextFieldValue) {
_searchFilterUnstaged.value = filter
}
fun stagedTreeDirectoryClicked(directoryPath: String) {
val contractedDirectories = treeContractedDirectories.value
if (contractedDirectories.contains(directoryPath)) {
treeContractedDirectories.value -= directoryPath
} else {
treeContractedDirectories.value += directoryPath
}
}
fun alternateShowAsTree() {
appSettings.showChangesAsTree = !appSettings.showChangesAsTree
}
fun stageByDirectory(dir: String) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITTED_CHANGES,
showError = true,
) { git ->
stageByDirectoryUseCase(git, dir)
}
fun unstageByDirectory(dir: String) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITTED_CHANGES,
showError = true,
) { git ->
unstageByDirectoryUseCase(git, dir)
}
}
sealed interface StageState {
data object Loading : StageState
data class Loaded(
val staged: List<StatusEntry>,
val stagedFiltered: List<StatusEntry>,
val unstaged: List<StatusEntry>,
val isPartiallyReloading: Boolean,
val unstagedFiltered: List<StatusEntry>,
val isPartiallyReloading: Boolean
) : StageState
}
sealed interface StageStateUi {
val hasStagedFiles: Boolean
val hasUnstagedFiles: Boolean
val isLoading: Boolean
val haveConflictsBeenSolved: Boolean
data object Loading : StageStateUi {
override val hasStagedFiles: Boolean
get() = false
override val hasUnstagedFiles: Boolean
get() = false
override val isLoading: Boolean
get() = true
override val haveConflictsBeenSolved: Boolean
get() = false
}
sealed interface Loaded : StageStateUi
data class TreeLoaded(
val staged: List<TreeItem<StatusEntry>>,
val unstaged: List<TreeItem<StatusEntry>>,
val isPartiallyReloading: Boolean,
) : Loaded {
override val hasStagedFiles: Boolean = staged.isNotEmpty()
override val hasUnstagedFiles: Boolean = unstaged.isNotEmpty()
override val isLoading: Boolean = isPartiallyReloading
override val haveConflictsBeenSolved: Boolean = unstaged.none {
it is TreeItem.File && it.data.statusType == StatusType.CONFLICTING
}
}
data class ListLoaded(
val staged: List<StatusEntry>,
val unstaged: List<StatusEntry>,
val isPartiallyReloading: Boolean,
) : Loaded {
override val hasStagedFiles: Boolean = staged.isNotEmpty()
override val hasUnstagedFiles: Boolean = unstaged.isNotEmpty()
override val isLoading: Boolean = isPartiallyReloading
override val haveConflictsBeenSolved: Boolean = unstaged.none { it.statusType == StatusType.CONFLICTING }
}
}
data class CommitMessage(val message: String, val messageType: MessageType)
enum class MessageType {
@ -579,7 +470,7 @@ enum class MessageType {
}
sealed interface CommitterDataRequestState {
data object None : CommitterDataRequestState
object None : CommitterDataRequestState
data class WaitingInput(val authorInfo: AuthorInfo) : CommitterDataRequestState
data class Accepted(val authorInfo: AuthorInfo, val persist: Boolean) : CommitterDataRequestState
object Reject : CommitterDataRequestState

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h240l80 80h320q33 0 56.5 23.5T880-640v400q0 33-23.5 56.5T800-160H160Zm0-80h640v-400H447l-80-80H160v480Zm0 0v-480 480Z"/></svg>

Before

Width:  |  Height:  |  Size: 282 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h240l80 80h320q33 0 56.5 23.5T880-640H447l-80-80H160v480l96-320h684L837-217q-8 26-29.5 41.5T760-160H160Zm84-80h516l72-240H316l-72 240Zm0 0 72-240-72 240Zm-84-400v-80 80Z"/></svg>

Before

Width:  |  Height:  |  Size: 334 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M600-120v-120H440v-400h-80v120H80v-320h280v120h240v-120h280v320H600v-120h-80v320h80v-120h280v320H600ZM160-760v160-160Zm520 400v160-160Zm0-400v160-160Zm0 160h120v-160H680v160Zm0 400h120v-160H680v160ZM160-600h120v-160H160v160Z"/></svg>

Before

Width:  |  Height:  |  Size: 330 B