parent
8cde01bc00
commit
8b36537451
@ -25,6 +25,8 @@ object AppIcons {
|
|||||||
const val ERROR = "error.svg"
|
const val ERROR = "error.svg"
|
||||||
const val EXPAND_MORE = "expand_more.svg"
|
const val EXPAND_MORE = "expand_more.svg"
|
||||||
const val FETCH = "fetch.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 GRADE = "grade.svg"
|
||||||
const val HORIZONTAL_SPLIT = "horizontal_split.svg"
|
const val HORIZONTAL_SPLIT = "horizontal_split.svg"
|
||||||
const val HISTORY = "history.svg"
|
const val HISTORY = "history.svg"
|
||||||
@ -59,6 +61,7 @@ object AppIcons {
|
|||||||
const val TAG = "tag.svg"
|
const val TAG = "tag.svg"
|
||||||
const val TERMINAL = "terminal.svg"
|
const val TERMINAL = "terminal.svg"
|
||||||
const val TOPIC = "topic.svg"
|
const val TOPIC = "topic.svg"
|
||||||
|
const val TREE = "tree.svg"
|
||||||
const val UNDO = "undo.svg"
|
const val UNDO = "undo.svg"
|
||||||
const val UNIFIED = "unified.svg"
|
const val UNIFIED = "unified.svg"
|
||||||
const val UPDATE = "update.svg"
|
const val UPDATE = "update.svg"
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
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,6 +31,7 @@ private const val PREF_DIFF_TYPE = "diffType"
|
|||||||
private const val PREF_DIFF_FULL_FILE = "diffFullFile"
|
private const val PREF_DIFF_FULL_FILE = "diffFullFile"
|
||||||
private const val PREF_SWAP_UNCOMMITTED_CHANGES = "inverseUncommittedChanges"
|
private const val PREF_SWAP_UNCOMMITTED_CHANGES = "inverseUncommittedChanges"
|
||||||
private const val PREF_TERMINAL_PATH = "terminalPath"
|
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_USE_PROXY = "useProxy"
|
||||||
private const val PREF_PROXY_TYPE = "proxyType"
|
private const val PREF_PROXY_TYPE = "proxyType"
|
||||||
private const val PREF_PROXY_HOST_NAME = "proxyHostName"
|
private const val PREF_PROXY_HOST_NAME = "proxyHostName"
|
||||||
@ -50,6 +51,7 @@ private const val PREF_VERIFY_SSL = "verifySsl"
|
|||||||
private const val DEFAULT_COMMITS_LIMIT = 1000
|
private const val DEFAULT_COMMITS_LIMIT = 1000
|
||||||
private const val DEFAULT_COMMITS_LIMIT_ENABLED = true
|
private const val DEFAULT_COMMITS_LIMIT_ENABLED = true
|
||||||
private const val DEFAULT_SWAP_UNCOMMITTED_CHANGES = false
|
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_CACHE_CREDENTIALS_IN_MEMORY = true
|
||||||
private const val DEFAULT_VERIFY_SSL = true
|
private const val DEFAULT_VERIFY_SSL = true
|
||||||
const val DEFAULT_UI_SCALE = -1f
|
const val DEFAULT_UI_SCALE = -1f
|
||||||
@ -67,6 +69,9 @@ class AppSettings @Inject constructor() {
|
|||||||
private val _swapUncommittedChangesFlow = MutableStateFlow(swapUncommittedChanges)
|
private val _swapUncommittedChangesFlow = MutableStateFlow(swapUncommittedChanges)
|
||||||
val swapUncommittedChangesFlow = _swapUncommittedChangesFlow.asStateFlow()
|
val swapUncommittedChangesFlow = _swapUncommittedChangesFlow.asStateFlow()
|
||||||
|
|
||||||
|
private val _showChangesAsTreeFlow = MutableStateFlow(showChangesAsTree)
|
||||||
|
val showChangesAsTreeFlow = _showChangesAsTreeFlow.asStateFlow()
|
||||||
|
|
||||||
private val _cacheCredentialsInMemoryFlow = MutableStateFlow(cacheCredentialsInMemory)
|
private val _cacheCredentialsInMemoryFlow = MutableStateFlow(cacheCredentialsInMemory)
|
||||||
val cacheCredentialsInMemoryFlow = _cacheCredentialsInMemoryFlow.asStateFlow()
|
val cacheCredentialsInMemoryFlow = _cacheCredentialsInMemoryFlow.asStateFlow()
|
||||||
|
|
||||||
@ -165,6 +170,15 @@ class AppSettings @Inject constructor() {
|
|||||||
_swapUncommittedChangesFlow.value = value
|
_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
|
var cacheCredentialsInMemory: Boolean
|
||||||
get() {
|
get() {
|
||||||
return preferences.getBoolean(PREF_CACHE_CREDENTIALS_IN_MEMORY, DEFAULT_CACHE_CREDENTIALS_IN_MEMORY)
|
return preferences.getBoolean(PREF_CACHE_CREDENTIALS_IN_MEMORY, DEFAULT_CACHE_CREDENTIALS_IN_MEMORY)
|
||||||
@ -347,18 +361,6 @@ class AppSettings @Inject constructor() {
|
|||||||
_customThemeFlow.value = Json.decodeFromString<ColorsScheme>(themeJson)
|
_customThemeFlow.value = Json.decodeFromString<ColorsScheme>(themeJson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadProxySettings() {
|
|
||||||
_proxyFlow.value = ProxySettings(
|
|
||||||
useProxy,
|
|
||||||
proxyType,
|
|
||||||
proxyHostName,
|
|
||||||
proxyPortNumber,
|
|
||||||
proxyUseAuth,
|
|
||||||
proxyHostUser,
|
|
||||||
proxyHostPassword,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ProxySettings(
|
data class ProxySettings(
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package com.jetpackduba.gitnuro.ui
|
package com.jetpackduba.gitnuro.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.ScrollState
|
import androidx.compose.foundation.ScrollState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@ -21,18 +20,17 @@ import androidx.compose.ui.text.AnnotatedString
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.jetpackduba.gitnuro.AppIcons
|
import com.jetpackduba.gitnuro.AppIcons
|
||||||
import com.jetpackduba.gitnuro.extensions.*
|
import com.jetpackduba.gitnuro.extensions.*
|
||||||
import com.jetpackduba.gitnuro.git.DiffEntryType
|
import com.jetpackduba.gitnuro.git.DiffEntryType
|
||||||
import com.jetpackduba.gitnuro.theme.backgroundSelected
|
|
||||||
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
|
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
|
||||||
import com.jetpackduba.gitnuro.theme.tertiarySurface
|
import com.jetpackduba.gitnuro.theme.tertiarySurface
|
||||||
import com.jetpackduba.gitnuro.ui.components.*
|
import com.jetpackduba.gitnuro.ui.components.*
|
||||||
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
|
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement
|
||||||
import com.jetpackduba.gitnuro.ui.context_menu.committedChangesEntriesContextMenuItems
|
import com.jetpackduba.gitnuro.ui.context_menu.committedChangesEntriesContextMenuItems
|
||||||
import com.jetpackduba.gitnuro.viewmodels.CommitChangesState
|
import com.jetpackduba.gitnuro.ui.tree_files.TreeItem
|
||||||
|
import com.jetpackduba.gitnuro.viewmodels.CommitChangesStateUi
|
||||||
import com.jetpackduba.gitnuro.viewmodels.CommitChangesViewModel
|
import com.jetpackduba.gitnuro.viewmodels.CommitChangesViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -53,28 +51,29 @@ fun CommitChanges(
|
|||||||
commitChangesViewModel.loadChanges(selectedItem.revCommit)
|
commitChangesViewModel.loadChanges(selectedItem.revCommit)
|
||||||
}
|
}
|
||||||
|
|
||||||
val commitChangesStatus = commitChangesViewModel.commitChangesState.collectAsState().value
|
val commitChangesStatus = commitChangesViewModel.commitChangesStateUi.collectAsState().value
|
||||||
val showSearch by commitChangesViewModel.showSearch.collectAsState()
|
val showSearch by commitChangesViewModel.showSearch.collectAsState()
|
||||||
val changesListScroll by commitChangesViewModel.changesLazyListState.collectAsState()
|
val changesListScroll by commitChangesViewModel.changesLazyListState.collectAsState()
|
||||||
val textScroll by commitChangesViewModel.textScroll.collectAsState()
|
val textScroll by commitChangesViewModel.textScroll.collectAsState()
|
||||||
|
val showAsTree by commitChangesViewModel.showAsTree.collectAsState()
|
||||||
|
|
||||||
var searchFilter by remember(commitChangesViewModel, showSearch, commitChangesStatus) {
|
var searchFilter by remember(commitChangesViewModel, showSearch, commitChangesStatus) {
|
||||||
mutableStateOf(commitChangesViewModel.searchFilter.value)
|
mutableStateOf(commitChangesViewModel.searchFilter.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (commitChangesStatus) {
|
when (commitChangesStatus) {
|
||||||
CommitChangesState.Loading -> {
|
CommitChangesStateUi.Loading -> {
|
||||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant)
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant)
|
||||||
}
|
}
|
||||||
|
|
||||||
is CommitChangesState.Loaded -> {
|
is CommitChangesStateUi.Loaded -> {
|
||||||
CommitChangesView(
|
CommitChangesView(
|
||||||
diffSelected = diffSelected,
|
diffSelected = diffSelected,
|
||||||
commit = commitChangesStatus.commit,
|
commitChangesStatus = commitChangesStatus,
|
||||||
changes = commitChangesStatus.changesFiltered,
|
|
||||||
onBlame = onBlame,
|
onBlame = onBlame,
|
||||||
onHistory = onHistory,
|
onHistory = onHistory,
|
||||||
showSearch = showSearch,
|
showSearch = showSearch,
|
||||||
|
showAsTree = showAsTree,
|
||||||
changesListScroll = changesListScroll,
|
changesListScroll = changesListScroll,
|
||||||
textScroll = textScroll,
|
textScroll = textScroll,
|
||||||
searchFilter = searchFilter,
|
searchFilter = searchFilter,
|
||||||
@ -86,38 +85,37 @@ fun CommitChanges(
|
|||||||
searchFilter = filter
|
searchFilter = filter
|
||||||
commitChangesViewModel.onSearchFilterChanged(filter)
|
commitChangesViewModel.onSearchFilterChanged(filter)
|
||||||
},
|
},
|
||||||
|
onDirectoryClicked = { commitChangesViewModel.onDirectoryClicked(it.fullPath) },
|
||||||
|
onAlternateShowAsTree = { commitChangesViewModel.alternateShowAsTree() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CommitChangesView(
|
private fun CommitChangesView(
|
||||||
commit: RevCommit,
|
commitChangesStatus: CommitChangesStateUi.Loaded,
|
||||||
changes: List<DiffEntry>,
|
|
||||||
diffSelected: DiffEntryType?,
|
diffSelected: DiffEntryType?,
|
||||||
changesListScroll: LazyListState,
|
changesListScroll: LazyListState,
|
||||||
textScroll: ScrollState,
|
textScroll: ScrollState,
|
||||||
showSearch: Boolean,
|
showSearch: Boolean,
|
||||||
|
showAsTree: Boolean,
|
||||||
searchFilter: TextFieldValue,
|
searchFilter: TextFieldValue,
|
||||||
onBlame: (String) -> Unit,
|
onBlame: (String) -> Unit,
|
||||||
onHistory: (String) -> Unit,
|
onHistory: (String) -> Unit,
|
||||||
onDiffSelected: (DiffEntry) -> Unit,
|
onDiffSelected: (DiffEntry) -> Unit,
|
||||||
onSearchFilterToggled: (Boolean) -> Unit,
|
onSearchFilterToggled: (Boolean) -> Unit,
|
||||||
onSearchFilterChanged: (TextFieldValue) -> 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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 8.dp, bottom = 8.dp)
|
.padding(end = 8.dp, bottom = 8.dp)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
val searchFocusRequester = remember { FocusRequester() }
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -126,92 +124,169 @@ fun CommitChangesView(
|
|||||||
.weight(1f, fill = true)
|
.weight(1f, fill = true)
|
||||||
.background(MaterialTheme.colors.background)
|
.background(MaterialTheme.colors.background)
|
||||||
) {
|
) {
|
||||||
Row(
|
Header(
|
||||||
modifier = Modifier
|
showSearch,
|
||||||
.fillMaxWidth()
|
searchFilter,
|
||||||
.height(34.dp)
|
onSearchFilterChanged,
|
||||||
.background(MaterialTheme.colors.tertiarySurface),
|
onSearchFilterToggled,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
showAsTree = showAsTree,
|
||||||
) {
|
onAlternateShowAsTree = onAlternateShowAsTree,
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(modifier = Modifier.weight(1f))
|
when (commitChangesStatus) {
|
||||||
|
is CommitChangesStateUi.ListLoaded -> {
|
||||||
|
val changes = commitChangesStatus.changes
|
||||||
|
|
||||||
IconButton(
|
ListCommitLogChanges(
|
||||||
onClick = {
|
diffSelected = diffSelected,
|
||||||
onSearchFilterToggled(!showSearch)
|
changesListScroll = changesListScroll,
|
||||||
|
diffEntries = changes,
|
||||||
|
onDiffSelected = onDiffSelected,
|
||||||
|
onGenerateContextMenu = { diffEntry ->
|
||||||
|
committedChangesEntriesContextMenuItems(
|
||||||
|
diffEntry,
|
||||||
|
onBlame = { onBlame(diffEntry.filePath) },
|
||||||
|
onHistory = { onHistory(diffEntry.filePath) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!showSearch)
|
is CommitChangesStateUi.TreeLoaded -> {
|
||||||
requestFocus = true
|
TreeCommitLogChanges(
|
||||||
},
|
diffSelected = diffSelected,
|
||||||
modifier = Modifier.handOnHover(),
|
changesListScroll = changesListScroll,
|
||||||
) {
|
treeItems = commitChangesStatus.changes,
|
||||||
Icon(
|
onDiffSelected = onDiffSelected,
|
||||||
painter = painterResource(AppIcons.SEARCH),
|
onGenerateContextMenu = { diffEntry ->
|
||||||
contentDescription = null,
|
committedChangesEntriesContextMenuItems(
|
||||||
modifier = Modifier.size(18.dp),
|
diffEntry,
|
||||||
tint = MaterialTheme.colors.onBackground,
|
onBlame = { onBlame(diffEntry.filePath) },
|
||||||
|
onHistory = { onHistory(diffEntry.filePath) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDirectoryClicked = onDirectoryClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showSearch) {
|
}
|
||||||
SearchTextField(
|
|
||||||
searchFilter = searchFilter,
|
|
||||||
onSearchFilterChanged = onSearchFilterChanged,
|
|
||||||
searchFocusRequester = searchFocusRequester,
|
|
||||||
onClose = { onSearchFilterToggled(false) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(showSearch, requestFocus) {
|
MessageAuthorFooter(commit, textScroll)
|
||||||
if (showSearch && requestFocus) {
|
}
|
||||||
searchFocusRequester.requestFocus()
|
}
|
||||||
requestFocus = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CommitLogChanges(
|
@Composable
|
||||||
diffSelected = diffSelected,
|
private fun Header(
|
||||||
changesListScroll = changesListScroll,
|
showSearch: Boolean,
|
||||||
diffEntries = changes,
|
searchFilter: TextFieldValue,
|
||||||
onDiffSelected = onDiffSelected,
|
onSearchFilterChanged: (TextFieldValue) -> Unit,
|
||||||
onBlame = onBlame,
|
onSearchFilterToggled: (Boolean) -> Unit,
|
||||||
onHistory = onHistory,
|
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(
|
||||||
|
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()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(if (showAsTree) AppIcons.LIST else AppIcons.TREE),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = MaterialTheme.colors.onBackground,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
IconButton(
|
||||||
modifier = Modifier
|
onClick = {
|
||||||
.fillMaxWidth()
|
onSearchFilterToggled(!showSearch)
|
||||||
.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)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -300,15 +375,13 @@ fun Author(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CommitLogChanges(
|
fun ListCommitLogChanges(
|
||||||
diffEntries: List<DiffEntry>,
|
diffEntries: List<DiffEntry>,
|
||||||
diffSelected: DiffEntryType?,
|
diffSelected: DiffEntryType?,
|
||||||
changesListScroll: LazyListState,
|
changesListScroll: LazyListState,
|
||||||
onBlame: (String) -> Unit,
|
|
||||||
onHistory: (String) -> Unit,
|
|
||||||
onDiffSelected: (DiffEntry) -> Unit,
|
onDiffSelected: (DiffEntry) -> Unit,
|
||||||
|
onGenerateContextMenu: (DiffEntry) -> List<ContextMenuElement>,
|
||||||
) {
|
) {
|
||||||
ScrollableLazyColumn(
|
ScrollableLazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -316,74 +389,96 @@ fun CommitLogChanges(
|
|||||||
state = changesListScroll,
|
state = changesListScroll,
|
||||||
) {
|
) {
|
||||||
items(items = diffEntries) { diffEntry ->
|
items(items = diffEntries) { diffEntry ->
|
||||||
ContextMenu(
|
FileEntry(
|
||||||
items = {
|
icon = diffEntry.icon,
|
||||||
committedChangesEntriesContextMenuItems(
|
iconColor = diffEntry.iconColor,
|
||||||
diffEntry,
|
parentDirectoryPath = diffEntry.parentDirectoryPath,
|
||||||
onBlame = { onBlame(diffEntry.filePath) },
|
fileName = diffEntry.fileName,
|
||||||
onHistory = { onHistory(diffEntry.filePath) },
|
isSelected = diffSelected is DiffEntryType.CommitDiff && diffSelected.diffEntry == diffEntry,
|
||||||
)
|
onClick = { onDiffSelected(diffEntry) },
|
||||||
}
|
onDoubleClick = {},
|
||||||
) {
|
onGenerateContextMenu = { onGenerateContextMenu(diffEntry) },
|
||||||
Column(
|
trailingAction = null,
|
||||||
modifier = Modifier
|
)
|
||||||
.height(40.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,
|
||||||
|
)
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,161 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
@ -220,7 +220,7 @@ fun showPopup(x: Int, y: Int, contextMenuElements: List<ContextMenuElement>, onD
|
|||||||
fun Separator() {
|
fun Separator() {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(1.dp)
|
.height(1.dp)
|
||||||
.background(MaterialTheme.colors.onBackground.copy(alpha = 0.4f))
|
.background(MaterialTheme.colors.onBackground.copy(alpha = 0.4f))
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
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,6 +10,9 @@ import com.jetpackduba.gitnuro.extensions.lowercaseContains
|
|||||||
import com.jetpackduba.gitnuro.git.RefreshType
|
import com.jetpackduba.gitnuro.git.RefreshType
|
||||||
import com.jetpackduba.gitnuro.git.TabState
|
import com.jetpackduba.gitnuro.git.TabState
|
||||||
import com.jetpackduba.gitnuro.git.diff.GetCommitDiffEntriesUseCase
|
import com.jetpackduba.gitnuro.git.diff.GetCommitDiffEntriesUseCase
|
||||||
|
import 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.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.eclipse.jgit.diff.DiffEntry
|
import org.eclipse.jgit.diff.DiffEntry
|
||||||
@ -21,6 +24,7 @@ private const val MIN_TIME_IN_MS_TO_SHOW_LOAD = 300L
|
|||||||
class CommitChangesViewModel @Inject constructor(
|
class CommitChangesViewModel @Inject constructor(
|
||||||
private val tabState: TabState,
|
private val tabState: TabState,
|
||||||
private val getCommitDiffEntriesUseCase: GetCommitDiffEntriesUseCase,
|
private val getCommitDiffEntriesUseCase: GetCommitDiffEntriesUseCase,
|
||||||
|
private val appSettings: AppSettings,
|
||||||
tabScope: CoroutineScope,
|
tabScope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
private val _showSearch = MutableStateFlow(false)
|
private val _showSearch = MutableStateFlow(false)
|
||||||
@ -33,26 +37,48 @@ class CommitChangesViewModel @Inject constructor(
|
|||||||
LazyListState(firstVisibleItemIndex = 0, firstVisibleItemScrollOffset = 0)
|
LazyListState(firstVisibleItemIndex = 0, firstVisibleItemScrollOffset = 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
val textScroll = MutableStateFlow(
|
val textScroll = MutableStateFlow(ScrollState(0))
|
||||||
ScrollState(0)
|
|
||||||
)
|
val showAsTree = appSettings.showChangesAsTreeFlow
|
||||||
|
private val treeContractedDirectories = MutableStateFlow(emptyList<String>())
|
||||||
|
|
||||||
private val _commitChangesState = MutableStateFlow<CommitChangesState>(CommitChangesState.Loading)
|
private val _commitChangesState = MutableStateFlow<CommitChangesState>(CommitChangesState.Loading)
|
||||||
val commitChangesState: StateFlow<CommitChangesState> =
|
|
||||||
|
private val commitChangesFiltered =
|
||||||
combine(_commitChangesState, _showSearch, _searchFilter) { state, showSearch, filter ->
|
combine(_commitChangesState, _showSearch, _searchFilter) { state, showSearch, filter ->
|
||||||
if (state is CommitChangesState.Loaded) {
|
if (state is CommitChangesState.Loaded && showSearch && filter.text.isNotBlank()) {
|
||||||
if (showSearch && filter.text.isNotBlank()) {
|
state.copy(changes = state.changes.filter { it.filePath.lowercaseContains(filter.text) })
|
||||||
state.copy(changesFiltered = state.changes.filter { it.filePath.lowercaseContains(filter.text) })
|
|
||||||
} else {
|
|
||||||
state
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
}.stateIn(
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
tabScope,
|
tabScope,
|
||||||
SharingStarted.Lazily,
|
SharingStarted.Lazily,
|
||||||
CommitChangesState.Loading
|
CommitChangesStateUi.Loading
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -92,7 +118,7 @@ class CommitChangesViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_commitChangesState.value = CommitChangesState.Loaded(commit, changes, changes)
|
_commitChangesState.value = CommitChangesState.Loaded(commit, changes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +132,20 @@ 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) {
|
fun onSearchFilterToggled(visible: Boolean) {
|
||||||
_showSearch.value = visible
|
_showSearch.value = visible
|
||||||
}
|
}
|
||||||
@ -115,9 +155,22 @@ class CommitChangesViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface CommitChangesState {
|
private sealed interface CommitChangesState {
|
||||||
data object Loading : CommitChangesState
|
data object Loading : CommitChangesState
|
||||||
data class Loaded(val commit: RevCommit, val changes: List<DiffEntry>, val changesFiltered: List<DiffEntry>) :
|
data class Loaded(val commit: RevCommit, val changes: List<DiffEntry>) :
|
||||||
CommitChangesState
|
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,6 +21,8 @@ import com.jetpackduba.gitnuro.git.repository.ResetRepositoryStateUseCase
|
|||||||
import com.jetpackduba.gitnuro.git.workspace.*
|
import com.jetpackduba.gitnuro.git.workspace.*
|
||||||
import com.jetpackduba.gitnuro.models.AuthorInfo
|
import com.jetpackduba.gitnuro.models.AuthorInfo
|
||||||
import com.jetpackduba.gitnuro.preferences.AppSettings
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
@ -38,6 +40,8 @@ class StatusViewModel @Inject constructor(
|
|||||||
private val tabState: TabState,
|
private val tabState: TabState,
|
||||||
private val stageEntryUseCase: StageEntryUseCase,
|
private val stageEntryUseCase: StageEntryUseCase,
|
||||||
private val unstageEntryUseCase: UnstageEntryUseCase,
|
private val unstageEntryUseCase: UnstageEntryUseCase,
|
||||||
|
private val stageByDirectoryUseCase: StageByDirectoryUseCase,
|
||||||
|
private val unstageByDirectoryUseCase: UnstageByDirectoryUseCase,
|
||||||
private val resetEntryUseCase: DiscardEntryUseCase,
|
private val resetEntryUseCase: DiscardEntryUseCase,
|
||||||
private val stageAllUseCase: StageAllUseCase,
|
private val stageAllUseCase: StageAllUseCase,
|
||||||
private val unstageAllUseCase: UnstageAllUseCase,
|
private val unstageAllUseCase: UnstageAllUseCase,
|
||||||
@ -55,8 +59,8 @@ class StatusViewModel @Inject constructor(
|
|||||||
private val saveAuthorUseCase: SaveAuthorUseCase,
|
private val saveAuthorUseCase: SaveAuthorUseCase,
|
||||||
private val sharedRepositoryStateManager: SharedRepositoryStateManager,
|
private val sharedRepositoryStateManager: SharedRepositoryStateManager,
|
||||||
private val getSpecificCommitMessageUseCase: GetSpecificCommitMessageUseCase,
|
private val getSpecificCommitMessageUseCase: GetSpecificCommitMessageUseCase,
|
||||||
|
private val appSettings: AppSettings,
|
||||||
tabScope: CoroutineScope,
|
tabScope: CoroutineScope,
|
||||||
appSettings: AppSettings,
|
|
||||||
) {
|
) {
|
||||||
private val _showSearchUnstaged = MutableStateFlow(false)
|
private val _showSearchUnstaged = MutableStateFlow(false)
|
||||||
val showSearchUnstaged: StateFlow<Boolean> = _showSearchUnstaged
|
val showSearchUnstaged: StateFlow<Boolean> = _showSearchUnstaged
|
||||||
@ -73,9 +77,11 @@ class StatusViewModel @Inject constructor(
|
|||||||
val swapUncommittedChanges = appSettings.swapUncommittedChangesFlow
|
val swapUncommittedChanges = appSettings.swapUncommittedChangesFlow
|
||||||
val rebaseInteractiveState = sharedRepositoryStateManager.rebaseInteractiveState
|
val rebaseInteractiveState = sharedRepositoryStateManager.rebaseInteractiveState
|
||||||
|
|
||||||
|
private val treeContractedDirectories = MutableStateFlow(emptyList<String>())
|
||||||
|
private val showAsTree = appSettings.showChangesAsTreeFlow
|
||||||
private val _stageState = MutableStateFlow<StageState>(StageState.Loading)
|
private val _stageState = MutableStateFlow<StageState>(StageState.Loading)
|
||||||
|
|
||||||
val stageState: StateFlow<StageState> = combine(
|
private val stageStateFiltered: StateFlow<StageState> = combine(
|
||||||
_stageState,
|
_stageState,
|
||||||
_showSearchStaged,
|
_showSearchStaged,
|
||||||
_searchFilterStaged,
|
_searchFilterStaged,
|
||||||
@ -83,7 +89,6 @@ class StatusViewModel @Inject constructor(
|
|||||||
_searchFilterUnstaged,
|
_searchFilterUnstaged,
|
||||||
) { state, showSearchStaged, filterStaged, showSearchUnstaged, filterUnstaged ->
|
) { state, showSearchStaged, filterStaged, showSearchUnstaged, filterUnstaged ->
|
||||||
if (state is StageState.Loaded) {
|
if (state is StageState.Loaded) {
|
||||||
|
|
||||||
val unstaged = if (showSearchUnstaged && filterUnstaged.text.isNotBlank()) {
|
val unstaged = if (showSearchUnstaged && filterUnstaged.text.isNotBlank()) {
|
||||||
state.unstaged.filter { it.filePath.lowercaseContains(filterUnstaged.text) }
|
state.unstaged.filter { it.filePath.lowercaseContains(filterUnstaged.text) }
|
||||||
} else {
|
} else {
|
||||||
@ -96,31 +101,49 @@ class StatusViewModel @Inject constructor(
|
|||||||
state.staged
|
state.staged
|
||||||
}.prioritizeConflicts()
|
}.prioritizeConflicts()
|
||||||
|
|
||||||
state.copy(stagedFiltered = staged, unstagedFiltered = unstaged)
|
state.copy(staged = staged, unstaged = unstaged)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
}.stateIn(
|
}
|
||||||
tabScope,
|
.stateIn(
|
||||||
SharingStarted.Lazily,
|
tabScope,
|
||||||
StageState.Loading
|
SharingStarted.Lazily,
|
||||||
)
|
StageState.Loading
|
||||||
|
)
|
||||||
|
|
||||||
fun List<StatusEntry>.prioritizeConflicts(): List<StatusEntry> {
|
|
||||||
return this.groupBy { it.filePath }
|
val stageStateUi: StateFlow<StageStateUi> = combine(
|
||||||
.map {
|
stageStateFiltered,
|
||||||
val statusEntries = it.value
|
showAsTree,
|
||||||
return@map if (statusEntries.count() == 1) {
|
treeContractedDirectories,
|
||||||
statusEntries.first()
|
) { 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,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
val conflictingEntry =
|
StageStateUi.ListLoaded(
|
||||||
statusEntries.firstOrNull { entry -> entry.statusType == StatusType.CONFLICTING }
|
staged = stageStateFiltered.staged,
|
||||||
|
unstaged = stageStateFiltered.unstaged,
|
||||||
conflictingEntry ?: statusEntries.first()
|
isPartiallyReloading = stageStateFiltered.isPartiallyReloading,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StageState.Loading -> StageStateUi.Loading
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.stateIn(
|
||||||
|
tabScope,
|
||||||
|
SharingStarted.Lazily,
|
||||||
|
StageStateUi.Loading
|
||||||
|
)
|
||||||
|
|
||||||
var savedCommitMessage = CommitMessage("", MessageType.NORMAL)
|
var savedCommitMessage = CommitMessage("", MessageType.NORMAL)
|
||||||
|
|
||||||
@ -248,9 +271,8 @@ class StatusViewModel @Inject constructor(
|
|||||||
|
|
||||||
_stageState.value = StageState.Loaded(
|
_stageState.value = StageState.Loaded(
|
||||||
staged = staged,
|
staged = staged,
|
||||||
stagedFiltered = staged,
|
|
||||||
unstaged = unstaged,
|
unstaged = unstaged,
|
||||||
unstagedFiltered = unstaged, isPartiallyReloading = false
|
isPartiallyReloading = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
@ -259,6 +281,21 @@ 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 {
|
private fun messageByRepoState(git: Git): String {
|
||||||
val message: String? = if (
|
val message: String? = if (
|
||||||
git.repository.repositoryState.isMerging ||
|
git.repository.repositoryState.isMerging ||
|
||||||
@ -449,19 +486,91 @@ class StatusViewModel @Inject constructor(
|
|||||||
fun onSearchFilterChangedUnstaged(filter: TextFieldValue) {
|
fun onSearchFilterChangedUnstaged(filter: TextFieldValue) {
|
||||||
_searchFilterUnstaged.value = filter
|
_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 {
|
sealed interface StageState {
|
||||||
data object Loading : StageState
|
data object Loading : StageState
|
||||||
data class Loaded(
|
data class Loaded(
|
||||||
val staged: List<StatusEntry>,
|
val staged: List<StatusEntry>,
|
||||||
val stagedFiltered: List<StatusEntry>,
|
|
||||||
val unstaged: List<StatusEntry>,
|
val unstaged: List<StatusEntry>,
|
||||||
val unstagedFiltered: List<StatusEntry>,
|
val isPartiallyReloading: Boolean,
|
||||||
val isPartiallyReloading: Boolean
|
|
||||||
) : StageState
|
) : 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)
|
data class CommitMessage(val message: String, val messageType: MessageType)
|
||||||
|
|
||||||
enum class MessageType {
|
enum class MessageType {
|
||||||
@ -470,7 +579,7 @@ enum class MessageType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed interface CommitterDataRequestState {
|
sealed interface CommitterDataRequestState {
|
||||||
object None : CommitterDataRequestState
|
data object None : CommitterDataRequestState
|
||||||
data class WaitingInput(val authorInfo: AuthorInfo) : CommitterDataRequestState
|
data class WaitingInput(val authorInfo: AuthorInfo) : CommitterDataRequestState
|
||||||
data class Accepted(val authorInfo: AuthorInfo, val persist: Boolean) : CommitterDataRequestState
|
data class Accepted(val authorInfo: AuthorInfo, val persist: Boolean) : CommitterDataRequestState
|
||||||
object Reject : CommitterDataRequestState
|
object Reject : CommitterDataRequestState
|
||||||
|
1
src/main/resources/folder.svg
Normal file
1
src/main/resources/folder.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 282 B |
1
src/main/resources/folder_open.svg
Normal file
1
src/main/resources/folder_open.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 334 B |
1
src/main/resources/tree.svg
Normal file
1
src/main/resources/tree.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 330 B |
Loading…
Reference in New Issue
Block a user