Compare commits
1 commit
main
...
lists_heig
Author | SHA1 | Date | |
---|---|---|---|
|
913b9c65e3 |
28 changed files with 563 additions and 1607 deletions
|
@ -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"
|
||||
|
|
|
@ -278,7 +278,7 @@ class App {
|
|||
},
|
||||
onTabClosed = onCloseTab,
|
||||
onAddNewTab = onAddedTab,
|
||||
tabsHeight = 40.dp,
|
||||
tabsHeight = 36.dp,
|
||||
onMoveTab = { fromIndex, toIndex ->
|
||||
tabsManager.onMoveTab(fromIndex, toIndex)
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = {
|
|
@ -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(
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()))
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
Loading…
Reference in a new issue