Implemented search in side panel

Refactored composables to use a lazy column instead of a normal column, as it had performance issues with large repositories.

Fixes #43
This commit is contained in:
Abdelilah El Aissaoui 2023-01-29 19:27:17 +01:00
parent 7de332be87
commit 9d07ac59b7
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
23 changed files with 666 additions and 506 deletions

View File

@ -0,0 +1,28 @@
package com.jetpackduba.gitnuro.di.factories
import com.jetpackduba.gitnuro.viewmodels.sidepanel.BranchesViewModel
import com.jetpackduba.gitnuro.viewmodels.sidepanel.RemotesViewModel
import com.jetpackduba.gitnuro.viewmodels.sidepanel.StashesViewModel
import com.jetpackduba.gitnuro.viewmodels.sidepanel.TagsViewModel
import dagger.assisted.AssistedFactory
import kotlinx.coroutines.flow.StateFlow
@AssistedFactory
interface BranchesViewModelFactory {
fun create(filter: StateFlow<String>): BranchesViewModel
}
@AssistedFactory
interface RemotesViewModelFactory {
fun create(filter: StateFlow<String>): RemotesViewModel
}
@AssistedFactory
interface TagsViewModelFactory {
fun create(filter: StateFlow<String>): TagsViewModel
}
@AssistedFactory
interface StashesViewModelFactory {
fun create(filter: StateFlow<String>): StashesViewModel
}

View File

@ -46,4 +46,8 @@ val String.lineDelimiter: String?
}
val String.nullIfEmpty: String?
get() = this.ifBlank { null }
get() = this.ifBlank { null }
fun String.lowercaseContains(other: String) : Boolean {
return this.lowercase().contains(other.lowercase().trim())
}

View File

@ -27,6 +27,7 @@ fun textFieldColors(
fun outlinedTextFieldColors() = TextFieldDefaults.outlinedTextFieldColors(
cursorColor = MaterialTheme.colors.primaryVariant,
focusedBorderColor = MaterialTheme.colors.primaryVariant,
unfocusedBorderColor = MaterialTheme.colors.onBackgroundSecondary.copy(alpha = 0.2f),
focusedLabelColor = MaterialTheme.colors.primaryVariant,
backgroundColor = MaterialTheme.colors.background,
textColor = MaterialTheme.colors.onBackground,

View File

@ -1,104 +0,0 @@
package com.jetpackduba.gitnuro.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.extensions.isLocal
import com.jetpackduba.gitnuro.extensions.simpleName
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
import com.jetpackduba.gitnuro.ui.components.SideMenuPanel
import com.jetpackduba.gitnuro.ui.components.SideMenuSubentry
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.branchContextMenuItems
import com.jetpackduba.gitnuro.viewmodels.BranchesViewModel
import org.eclipse.jgit.lib.Ref
@Composable
fun Branches(
branchesViewModel: BranchesViewModel = gitnuroViewModel(),
) {
val branches by branchesViewModel.branches.collectAsState()
val currentBranchState = branchesViewModel.currentBranch.collectAsState()
val isExpanded by branchesViewModel.isExpanded.collectAsState()
val currentBranch = currentBranchState.value
SideMenuPanel(
title = "Local branches",
icon = painterResource("branch.svg"),
items = branches,
isExpanded = isExpanded,
onExpand = { branchesViewModel.onExpand() },
itemContent = { branch ->
BranchLineEntry(
branch = branch,
currentBranch = currentBranch,
isCurrentBranch = currentBranch?.name == branch.name,
onBranchClicked = { branchesViewModel.selectBranch(branch) },
onBranchDoubleClicked = { branchesViewModel.checkoutRef(branch) },
onCheckoutBranch = { branchesViewModel.checkoutRef(branch) },
onMergeBranch = { branchesViewModel.mergeBranch(branch) },
onDeleteBranch = { branchesViewModel.deleteBranch(branch) },
onRebaseBranch = { branchesViewModel.rebaseBranch(branch) },
onPushToRemoteBranch = { branchesViewModel.pushToRemoteBranch(branch) },
onPullFromRemoteBranch = { branchesViewModel.pullFromRemoteBranch(branch) },
)
}
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun BranchLineEntry(
branch: Ref,
currentBranch: Ref?,
isCurrentBranch: Boolean,
onBranchClicked: () -> Unit,
onBranchDoubleClicked: () -> Unit,
onCheckoutBranch: () -> Unit,
onMergeBranch: () -> Unit,
onRebaseBranch: () -> Unit,
onDeleteBranch: () -> Unit,
onPushToRemoteBranch: () -> Unit,
onPullFromRemoteBranch: () -> Unit,
) {
ContextMenu(
items = {
branchContextMenuItems(
branch = branch,
currentBranch = currentBranch,
isCurrentBranch = isCurrentBranch,
isLocal = branch.isLocal,
onCheckoutBranch = onCheckoutBranch,
onMergeBranch = onMergeBranch,
onDeleteBranch = onDeleteBranch,
onRebaseBranch = onRebaseBranch,
onPushToRemoteBranch = onPushToRemoteBranch,
onPullFromRemoteBranch = onPullFromRemoteBranch,
)
}
) {
SideMenuSubentry(
text = branch.simpleName,
iconResourcePath = "branch.svg",
onClick = onBranchClicked,
onDoubleClick = onBranchDoubleClicked,
) {
if (isCurrentBranch) {
Text(
text = "HEAD",
color = MaterialTheme.colors.onBackgroundSecondary,
style = MaterialTheme.typography.caption,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
}
}
}

View File

@ -1,123 +0,0 @@
@file:OptIn(ExperimentalComposeUiApi::class)
package com.jetpackduba.gitnuro.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.extensions.handOnHover
import com.jetpackduba.gitnuro.extensions.simpleName
import com.jetpackduba.gitnuro.ui.components.SideMenuPanel
import com.jetpackduba.gitnuro.ui.components.SideMenuSubentry
import com.jetpackduba.gitnuro.ui.components.VerticalExpandable
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.remoteBranchesContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.remoteContextMenu
import com.jetpackduba.gitnuro.ui.dialogs.EditRemotesDialog
import com.jetpackduba.gitnuro.viewmodels.RemoteView
import com.jetpackduba.gitnuro.viewmodels.RemotesViewModel
import org.eclipse.jgit.lib.Ref
@Composable
fun Remotes(
remotesViewModel: RemotesViewModel = gitnuroViewModel(),
) {
val remotes by remotesViewModel.remotes.collectAsState()
var showEditRemotesDialog by remember { mutableStateOf(false) }
val isExpanded by remotesViewModel.isExpanded.collectAsState()
if (showEditRemotesDialog) {
EditRemotesDialog(
remotesViewModel = remotesViewModel,
onDismiss = {
showEditRemotesDialog = false
},
)
}
SideMenuPanel(
title = "Remotes",
icon = painterResource("cloud.svg"),
items = remotes,
isExpanded = isExpanded,
onExpand = { remotesViewModel.onExpand() },
contextItems = {
remoteContextMenu { showEditRemotesDialog = true }
},
headerHoverIcon = {
IconButton(
onClick = { showEditRemotesDialog = true },
modifier = Modifier
.padding(end = 16.dp)
.size(16.dp)
.handOnHover(),
) {
Icon(
painter = painterResource("settings.svg"),
contentDescription = null,
modifier = Modifier
.fillMaxSize(),
tint = MaterialTheme.colors.onBackground,
)
}
},
itemContent = { remoteInfo ->
RemoteRow(
remote = remoteInfo,
onBranchClicked = { branch -> remotesViewModel.selectBranch(branch) },
onDeleteBranch = { branch -> remotesViewModel.deleteRemoteBranch(branch) },
onRemoteClicked = { remotesViewModel.onRemoteClicked(remoteInfo) }
)
},
)
}
@Composable
private fun RemoteRow(
remote: RemoteView,
onRemoteClicked: () -> Unit,
onBranchClicked: (Ref) -> Unit,
onDeleteBranch: (Ref) -> Unit,
) {
VerticalExpandable(
isExpanded = remote.isExpanded,
onExpand = onRemoteClicked,
header = {
SideMenuSubentry(
text = remote.remoteInfo.remoteConfig.name,
iconResourcePath = "cloud.svg",
)
}
) {
val branches = remote.remoteInfo.branchesList
Column {
branches.forEach { branch ->
ContextMenu(
items = {
remoteBranchesContextMenu(
onDeleteBranch = { onDeleteBranch(branch) }
)
}
) {
SideMenuSubentry(
text = branch.simpleName,
extraPadding = 24.dp,
iconResourcePath = "branch.svg",
onClick = { onBranchClicked(branch) }
)
}
}
}
}
}

View File

@ -5,7 +5,6 @@ package com.jetpackduba.gitnuro.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
@ -17,17 +16,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.keybindings.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.ui.components.PrimaryButton
import com.jetpackduba.gitnuro.ui.components.ScrollableColumn
import com.jetpackduba.gitnuro.ui.dialogs.*
import com.jetpackduba.gitnuro.ui.diff.Diff
import com.jetpackduba.gitnuro.ui.log.Log
@ -252,39 +248,6 @@ fun RepoContent(
}
}
@Composable
fun SidePanelOption(title: String, icon: String, onClick: () -> Unit) {
Row(
modifier = Modifier
.height(36.dp)
.fillMaxWidth()
.handMouseClickable(onClick)
.padding(start = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(icon),
contentDescription = null,
tint = MaterialTheme.colors.onBackground,
modifier = Modifier
.size(16.dp),
)
Text(
text = title,
modifier = Modifier
.padding(horizontal = 8.dp)
.weight(1f),
maxLines = 1,
style = MaterialTheme.typography.body2,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colors.onBackground,
overflow = TextOverflow.Ellipsis,
)
}
}
@OptIn(ExperimentalSplitPaneApi::class)
@Composable
fun MainContentView(
@ -298,18 +261,7 @@ fun MainContentView(
splitPaneState = rememberSplitPaneState(initialPositionPercentage = 0.20f)
) {
first(minSize = 180.dp) {
Column {
ScrollableColumn(
modifier = Modifier
.weight(1f),
) {
Branches()
Remotes()
Tags()
Stashes()
// TODO: Enable on 1.2.0 when fully implemented Submodules()
}
}
SidePanel()
}
splitter {

View File

@ -0,0 +1,419 @@
package com.jetpackduba.gitnuro.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.extensions.handOnHover
import com.jetpackduba.gitnuro.extensions.isLocal
import com.jetpackduba.gitnuro.extensions.simpleName
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
import com.jetpackduba.gitnuro.ui.components.*
import com.jetpackduba.gitnuro.ui.context_menu.*
import com.jetpackduba.gitnuro.ui.dialogs.EditRemotesDialog
import com.jetpackduba.gitnuro.viewmodels.sidepanel.*
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.revwalk.RevCommit
@Composable
fun SidePanel(
sidePanelViewModel: SidePanelViewModel = gitnuroViewModel(),
branchesViewModel: BranchesViewModel = sidePanelViewModel.branchesViewModel,
remotesViewModel: RemotesViewModel = sidePanelViewModel.remotesViewModel,
tagsViewModel: TagsViewModel = sidePanelViewModel.tagsViewModel,
stashesViewModel: StashesViewModel = sidePanelViewModel.stashesViewModel,
) {
var filter by remember(sidePanelViewModel) { mutableStateOf(sidePanelViewModel.filter.value) }
val branchesState by branchesViewModel.branchesState.collectAsState()
val remotesState by remotesViewModel.remoteState.collectAsState()
val tagsState by tagsViewModel.tagsState.collectAsState()
val stashesState by stashesViewModel.stashesState.collectAsState()
var showEditRemotesDialog by remember { mutableStateOf(false) }
Column {
FilterTextField(
value = filter,
onValueChange = { newValue ->
filter = newValue
sidePanelViewModel.newFilter(newValue)
},
modifier = Modifier
.padding(start = 8.dp)
)
ScrollableLazyColumn(modifier = Modifier
.fillMaxSize()
.padding(top = 4.dp)
) {
localBranches(
branchesState = branchesState,
branchesViewModel = branchesViewModel,
)
remotes(
remotesState = remotesState,
remotesViewModel = remotesViewModel,
onShowEditRemotesDialog = { showEditRemotesDialog = true },
)
tags(
tagsState = tagsState,
tagsViewModel = tagsViewModel,
)
stashes(
stashesState = stashesState,
stashesViewModel = stashesViewModel,
)
}
}
if (showEditRemotesDialog) {
EditRemotesDialog(
remotesViewModel = remotesViewModel,
onDismiss = {
showEditRemotesDialog = false
},
)
}
}
@Composable
fun FilterTextField(value: String, onValueChange: (String) -> Unit, modifier: Modifier) {
AdjustableOutlinedTextField(
value = value,
hint = "Search for branches, tags & more",
onValueChange = onValueChange,
modifier = modifier,
textStyle = LocalTextStyle.current.copy(
fontSize = MaterialTheme.typography.body2.fontSize,
color = MaterialTheme.colors.onBackground,
),
maxLines = 1,
leadingIcon = {
Icon(
painterResource("search.svg"),
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = if (value.isEmpty()) MaterialTheme.colors.onBackgroundSecondary else MaterialTheme.colors.onBackground
)
}
)
}
fun LazyListScope.localBranches(
branchesState: BranchesState,
branchesViewModel: BranchesViewModel,
) {
val isExpanded = branchesState.isExpanded
val branches = branchesState.branches
val currentBranch = branchesState.currentBranch
item {
ContextMenu(
items = { emptyList() }
) {
SideMenuHeader(
text = "Local branches",
icon = painterResource("branch.svg"),
itemsCount = branches.count(),
hoverIcon = null,
isExpanded = isExpanded,
onExpand = { branchesViewModel.onExpand() }
)
}
}
if (isExpanded) {
items(branches, key = { it.name }) { branch ->
Branch(
branch = branch,
currentBranch = currentBranch,
isCurrentBranch = currentBranch?.name == branch.name,
onBranchClicked = { branchesViewModel.selectBranch(branch) },
onBranchDoubleClicked = { branchesViewModel.checkoutRef(branch) },
onCheckoutBranch = { branchesViewModel.checkoutRef(branch) },
onMergeBranch = { branchesViewModel.mergeBranch(branch) },
onDeleteBranch = { branchesViewModel.deleteBranch(branch) },
onRebaseBranch = { branchesViewModel.rebaseBranch(branch) },
onPushToRemoteBranch = { branchesViewModel.pushToRemoteBranch(branch) },
onPullFromRemoteBranch = { branchesViewModel.pullFromRemoteBranch(branch) },
)
}
}
}
fun LazyListScope.remotes(
remotesState: RemotesState,
remotesViewModel: RemotesViewModel,
onShowEditRemotesDialog: () -> Unit,
) {
val isExpanded = remotesState.isExpanded
val remotes = remotesState.remotes
item {
ContextMenu(
items = { remoteBranchesContextMenu(onShowEditRemotesDialog) }
) {
SideMenuHeader(
text = "Remotes",
icon = painterResource("cloud.svg"),
itemsCount = remotes.count(),
hoverIcon = {
IconButton(
onClick = onShowEditRemotesDialog,
modifier = Modifier
.padding(end = 16.dp)
.size(16.dp)
.handOnHover(),
) {
Icon(
painter = painterResource("settings.svg"),
contentDescription = null,
modifier = Modifier
.fillMaxSize(),
tint = MaterialTheme.colors.onBackground,
)
}
},
isExpanded = isExpanded,
onExpand = { remotesViewModel.onExpand() }
)
}
}
if (isExpanded) {
for (remote in remotes) {
item {
Remote(
remote = remote,
onRemoteClicked = { remotesViewModel.onRemoteClicked(remote) }
)
}
if (remote.isExpanded) {
items(remote.remoteInfo.branchesList) { remoteBranch ->
RemoteBranches(
remoteBranch = remoteBranch,
onBranchClicked = { remotesViewModel.selectBranch(remoteBranch) },
onDeleteBranch = { remotesViewModel.deleteRemoteBranch(remoteBranch) },
)
}
}
}
}
}
fun LazyListScope.tags(
tagsState: TagsState,
tagsViewModel: TagsViewModel,
) {
val isExpanded = tagsState.isExpanded
val tags = tagsState.tags
item {
ContextMenu(
items = { emptyList() }
) {
SideMenuHeader(
text = "Tags",
icon = painterResource("tag.svg"),
itemsCount = tags.count(),
hoverIcon = null,
isExpanded = isExpanded,
onExpand = { tagsViewModel.onExpand() }
)
}
}
if (isExpanded) {
items(tags, key = { it.name }) { tag ->
// if () {
Tag(
tag,
onTagClicked = { tagsViewModel.selectTag(tag) },
onCheckoutTag = { tagsViewModel.checkoutRef(tag) },
onDeleteTag = { tagsViewModel.deleteTag(tag) }
)
// }
}
}
}
fun LazyListScope.stashes(
stashesState: StashesState,
stashesViewModel: StashesViewModel,
) {
val isExpanded = stashesState.isExpanded
val stashes = stashesState.stashes
item {
ContextMenu(
items = { emptyList() }
) {
SideMenuHeader(
text = "Stashes",
icon = painterResource("stash.svg"),
itemsCount = stashes.count(),
hoverIcon = null,
isExpanded = isExpanded,
onExpand = { stashesViewModel.onExpand() }
)
}
}
if (isExpanded) {
items(stashes, key = { it.name }) { stash ->
Stash(
stash,
onClick = { stashesViewModel.selectStash(stash) },
onApply = { stashesViewModel.applyStash(stash) },
onPop = { stashesViewModel.popStash(stash) },
onDelete = { stashesViewModel.deleteStash(stash) },
)
}
}
}
@Composable
private fun Branch(
branch: Ref,
currentBranch: Ref?,
isCurrentBranch: Boolean,
onBranchClicked: () -> Unit,
onBranchDoubleClicked: () -> Unit,
onCheckoutBranch: () -> Unit,
onMergeBranch: () -> Unit,
onRebaseBranch: () -> Unit,
onDeleteBranch: () -> Unit,
onPushToRemoteBranch: () -> Unit,
onPullFromRemoteBranch: () -> Unit,
) {
ContextMenu(
items = {
branchContextMenuItems(
branch = branch,
currentBranch = currentBranch,
isCurrentBranch = isCurrentBranch,
isLocal = branch.isLocal,
onCheckoutBranch = onCheckoutBranch,
onMergeBranch = onMergeBranch,
onDeleteBranch = onDeleteBranch,
onRebaseBranch = onRebaseBranch,
onPushToRemoteBranch = onPushToRemoteBranch,
onPullFromRemoteBranch = onPullFromRemoteBranch,
)
}
) {
SideMenuSubentry(
text = branch.simpleName,
iconResourcePath = "branch.svg",
onClick = onBranchClicked,
onDoubleClick = onBranchDoubleClicked,
) {
if (isCurrentBranch) {
Text(
text = "HEAD",
color = MaterialTheme.colors.onBackgroundSecondary,
style = MaterialTheme.typography.caption,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
}
}
}
@Composable
private fun Remote(
remote: RemoteView,
onRemoteClicked: () -> Unit,
) {
SideMenuSubentry(
text = remote.remoteInfo.remoteConfig.name,
iconResourcePath = "cloud.svg",
onClick = onRemoteClicked
)
}
@Composable
private fun RemoteBranches(
remoteBranch: Ref,
onBranchClicked: () -> Unit,
onDeleteBranch: () -> Unit,
) {
ContextMenu(
items = {
remoteBranchesContextMenu(
onDeleteBranch = onDeleteBranch
)
}
) {
SideMenuSubentry(
text = remoteBranch.simpleName,
extraPadding = 24.dp,
iconResourcePath = "branch.svg",
onClick = onBranchClicked
)
}
}
@Composable
private fun Tag(
tag: Ref,
onTagClicked: () -> Unit,
onCheckoutTag: () -> Unit,
onDeleteTag: () -> Unit,
) {
ContextMenu(
items = {
tagContextMenuItems(
onCheckoutTag = onCheckoutTag,
onDeleteTag = onDeleteTag,
)
}
) {
SideMenuSubentry(
text = tag.simpleName,
iconResourcePath = "tag.svg",
onClick = onTagClicked,
)
}
}
@Composable
private fun Stash(
stash: RevCommit,
onClick: () -> Unit,
onApply: () -> Unit,
onPop: () -> Unit,
onDelete: () -> Unit,
) {
ContextMenu(
items = {
stashesContextMenuItems(
onApply = onApply,
onPop = onPop,
onDelete = onDelete,
)
}
) {
SideMenuSubentry(
text = stash.shortMessage,
iconResourcePath = "stash.svg",
onClick = onClick,
)
}
}

View File

@ -1,70 +0,0 @@
package com.jetpackduba.gitnuro.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.painterResource
import com.jetpackduba.gitnuro.ui.components.SideMenuPanel
import com.jetpackduba.gitnuro.ui.components.SideMenuSubentry
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement
import com.jetpackduba.gitnuro.ui.context_menu.stashesContextMenuItems
import com.jetpackduba.gitnuro.viewmodels.StashStatus
import com.jetpackduba.gitnuro.viewmodels.StashesViewModel
import org.eclipse.jgit.revwalk.RevCommit
@Composable
fun Stashes(
stashesViewModel: StashesViewModel = gitnuroViewModel(),
) {
val stashStatusState = stashesViewModel.stashStatus.collectAsState()
val stashStatus = stashStatusState.value
val isExpanded by stashesViewModel.isExpanded.collectAsState()
val stashList = if (stashStatus is StashStatus.Loaded)
stashStatus.stashes
else
listOf()
SideMenuPanel(
title = "Stashes",
icon = painterResource("stash.svg"),
items = stashList,
isExpanded = isExpanded,
onExpand = { stashesViewModel.onExpand() },
itemContent = { stash ->
StashRow(
stash = stash,
onClick = { stashesViewModel.selectTab(stash) },
contextItems = stashesContextMenuItems(
onApply = { stashesViewModel.applyStash(stash) },
onPop = {
stashesViewModel.popStash(stash)
},
onDelete = {
stashesViewModel.deleteStash(stash)
},
)
)
}
)
}
@Composable
private fun StashRow(
stash: RevCommit,
onClick: () -> Unit,
contextItems: List<ContextMenuElement>,
) {
ContextMenu(
items = { contextItems }
) {
SideMenuSubentry(
text = stash.shortMessage,
iconResourcePath = "stash.svg",
onClick = onClick,
)
}
}

View File

@ -16,7 +16,7 @@ import com.jetpackduba.gitnuro.ui.components.Tooltip
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.submoduleContextMenuItems
import com.jetpackduba.gitnuro.viewmodels.SubmodulesViewModel
import com.jetpackduba.gitnuro.viewmodels.sidepanel.SubmodulesViewModel
import org.eclipse.jgit.submodule.SubmoduleStatus
@Composable

View File

@ -1,65 +0,0 @@
package com.jetpackduba.gitnuro.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.painterResource
import com.jetpackduba.gitnuro.extensions.simpleName
import com.jetpackduba.gitnuro.ui.components.SideMenuPanel
import com.jetpackduba.gitnuro.ui.components.SideMenuSubentry
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.tagContextMenuItems
import com.jetpackduba.gitnuro.viewmodels.TagsViewModel
import org.eclipse.jgit.lib.Ref
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Tags(
tagsViewModel: TagsViewModel = gitnuroViewModel(),
) {
val tagsState = tagsViewModel.tags.collectAsState()
val tags = tagsState.value
val isExpanded by tagsViewModel.isExpanded.collectAsState()
SideMenuPanel(
title = "Tags",
items = tags,
icon = painterResource("tag.svg"),
isExpanded = isExpanded,
onExpand = { tagsViewModel.onExpand() },
itemContent = { tag ->
TagRow(
tag = tag,
onTagClicked = { tagsViewModel.selectTag(tag) },
onCheckoutTag = { tagsViewModel.checkoutRef(tag) },
onDeleteTag = { tagsViewModel.deleteTag(tag) }
)
}
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun TagRow(
tag: Ref,
onTagClicked: () -> Unit,
onCheckoutTag: () -> Unit,
onDeleteTag: () -> Unit,
) {
ContextMenu(
items = {
tagContextMenuItems(
onCheckoutTag = onCheckoutTag,
onDeleteTag = onDeleteTag,
)
}
) {
SideMenuSubentry(
text = tag.simpleName,
iconResourcePath = "tag.svg",
onClick = onTagClicked,
)
}
}

View File

@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
import com.jetpackduba.gitnuro.theme.outlinedTextFieldColors
@ -46,6 +47,7 @@ fun AdjustableOutlinedTextField(
shape: Shape = RoundedCornerShape(4.dp),
backgroundColor: Color = MaterialTheme.colors.background,
visualTransformation: VisualTransformation = VisualTransformation.None,
leadingIcon: @Composable (() -> Unit)? = null,
) {
val textColor = textStyle.color.takeOrElse {
colors.textColor(enabled).value
@ -60,7 +62,7 @@ fun AdjustableOutlinedTextField(
) {
BasicTextField(
modifier = Modifier
.heightIn(min = 40.dp)
.heightIn(min = 38.dp)
.background(backgroundColor)
.fillMaxWidth(),
value = value,
@ -74,29 +76,44 @@ fun AdjustableOutlinedTextField(
singleLine = singleLine,
visualTransformation = visualTransformation,
decorationBox = { innerTextField ->
Box(
Row(
modifier = Modifier
.border(
width = 1.dp,
width = 2.dp,
color = indicatorColor,
shape = shape
)
.padding(horizontal = 12.dp),
contentAlignment = Alignment.CenterStart,
verticalAlignment = Alignment.CenterVertically,
) {
if (leadingIcon != null) {
leadingIcon()
Spacer(modifier = Modifier.width(8.dp))
}
innerTextField()
}
}
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxSize()
.padding(start = 12.dp),
) {
if (leadingIcon != null) {
leadingIcon()
Spacer(modifier = Modifier.width(8.dp))
}
if (value.isEmpty() && hint.isNotEmpty()) {
Text(
hint,
color = MaterialTheme.colors.onBackgroundSecondary,
modifier = Modifier
.padding(start = 12.dp, top = 12.dp),
style = MaterialTheme.typography.body2
)
if (value.isEmpty() && hint.isNotEmpty()) {
Text(
hint,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colors.onBackgroundSecondary,
style = MaterialTheme.typography.body2
)
}
}
}
}

View File

@ -17,15 +17,17 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
@Composable
fun SideMenuEntry(
fun SideMenuHeader(
text: String,
icon: Painter? = null,
itemsCount: Int,
isExpanded: Boolean,
onExpand: () -> Unit = {},
hoverIcon: @Composable (() -> Unit)? = null,
) {
val hoverInteraction = remember { MutableInteractionSource() }
@ -35,7 +37,8 @@ fun SideMenuEntry(
modifier = Modifier
.height(36.dp)
.fillMaxWidth()
.hoverable(hoverInteraction),
.hoverable(hoverInteraction)
.handMouseClickable { onExpand() },
verticalAlignment = Alignment.CenterVertically,
) {
Icon(

View File

@ -26,7 +26,7 @@ fun <T> SideMenuPanel(
ContextMenu(
items = contextItems
) {
SideMenuEntry(
SideMenuHeader(
text = title,
icon = icon,
itemsCount = items.count(),

View File

@ -4,7 +4,10 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.runtime.*
@ -16,12 +19,11 @@ import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.extensions.handOnHover
import com.jetpackduba.gitnuro.theme.backgroundSelected
import com.jetpackduba.gitnuro.theme.outlinedTextFieldColors
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
import com.jetpackduba.gitnuro.theme.textButtonColors
import com.jetpackduba.gitnuro.theme.outlinedTextFieldColors
import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField
import com.jetpackduba.gitnuro.ui.components.PrimaryButton
import com.jetpackduba.gitnuro.viewmodels.RemotesViewModel
import com.jetpackduba.gitnuro.viewmodels.sidepanel.RemotesViewModel
import org.eclipse.jgit.transport.RemoteConfig
@Composable
@ -38,7 +40,8 @@ fun EditRemotesDialog(
)
}
val remotes by remotesViewModel.remotes.collectAsState()
val remotesState by remotesViewModel.remoteState.collectAsState()
val remotes = remotesState.remotes
var remoteChanged by remember { mutableStateOf(false) }
val selectedRemote = remotesEditorData.selectedRemote
@ -343,7 +346,7 @@ data class RemoteWrapper(
) {
val haveUrisChanged: Boolean = isNew ||
fetchUri != originalFetchUri ||
pushUri.toString() != originalPushUri
pushUri != originalPushUri
}

View File

@ -1,22 +1,19 @@
package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.di.TabScope
import com.jetpackduba.gitnuro.viewmodels.sidepanel.*
import javax.inject.Inject
import javax.inject.Provider
@TabScope
class TabViewModelsHolder @Inject constructor(
logViewModel: LogViewModel,
branchesViewModel: BranchesViewModel,
tagsViewModel: TagsViewModel,
remotesViewModel: RemotesViewModel,
statusViewModel: StatusViewModel,
menuViewModel: MenuViewModel,
stashesViewModel: StashesViewModel,
submodulesViewModel: SubmodulesViewModel,
commitChangesViewModel: CommitChangesViewModel,
cloneViewModel: CloneViewModel,
settingsViewModel: SettingsViewModel,
sidePanelViewModel: SidePanelViewModel,
// Dynamic VM
private val diffViewModelProvider: Provider<DiffViewModel>,
private val rebaseInteractiveViewModelProvider: Provider<RebaseInteractiveViewModel>,
@ -25,13 +22,9 @@ class TabViewModelsHolder @Inject constructor(
) {
val viewModels = mapOf(
logViewModel::class to logViewModel,
branchesViewModel::class to branchesViewModel,
tagsViewModel::class to tagsViewModel,
remotesViewModel::class to remotesViewModel,
sidePanelViewModel::class to sidePanelViewModel,
statusViewModel::class to statusViewModel,
menuViewModel::class to menuViewModel,
stashesViewModel::class to stashesViewModel,
submodulesViewModel::class to submodulesViewModel,
commitChangesViewModel::class to commitChangesViewModel,
cloneViewModel::class to cloneViewModel,
settingsViewModel::class to settingsViewModel,

View File

@ -1,5 +1,7 @@
package com.jetpackduba.gitnuro.viewmodels
package com.jetpackduba.gitnuro.viewmodels.sidepanel
import com.jetpackduba.gitnuro.extensions.lowercaseContains
import com.jetpackduba.gitnuro.extensions.simpleName
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.branches.*
@ -7,9 +9,10 @@ import com.jetpackduba.gitnuro.git.rebase.RebaseBranchUseCase
import com.jetpackduba.gitnuro.git.remote_operations.PullFromSpecificBranchUseCase
import com.jetpackduba.gitnuro.git.remote_operations.PushToSpecificBranchUseCase
import com.jetpackduba.gitnuro.preferences.AppSettings
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
@ -17,7 +20,8 @@ import javax.inject.Inject
private const val TAG = "BranchesViewModel"
class BranchesViewModel @Inject constructor(
class BranchesViewModel @AssistedInject constructor(
private val rebaseBranchUseCase: RebaseBranchUseCase,
private val tabState: TabState,
private val appSettings: AppSettings,
@ -30,16 +34,26 @@ class BranchesViewModel @Inject constructor(
private val deleteBranchUseCase: DeleteBranchUseCase,
private val checkoutRefUseCase: CheckoutRefUseCase,
private val tabScope: CoroutineScope,
) : ExpandableViewModel(true) {
@Assisted
private val filter: StateFlow<String>
) : SidePanelChildViewModel(true) {
private val _branches = MutableStateFlow<List<Ref>>(listOf())
val branches: StateFlow<List<Ref>>
get() = _branches
private val _currentBranch = MutableStateFlow<Ref?>(null)
val currentBranch: StateFlow<Ref?>
get() = _currentBranch
val branchesState = combine(_branches, _currentBranch, isExpanded, filter) { branches, currentBranch, isExpanded, filter ->
BranchesState(
branches = branches.filter { it.simpleName.lowercaseContains(filter) },
isExpanded = isExpanded,
currentBranch = currentBranch
)
}.stateIn(
scope = tabScope,
started = SharingStarted.Eagerly,
initialValue = BranchesState(emptyList(), isExpanded.value, null)
)
init {
tabScope.launch {
tabState.refreshFlowFiltered(RefreshType.ALL_DATA)
{
@ -116,4 +130,10 @@ class BranchesViewModel @Inject constructor(
remoteBranch = branch,
)
}
}
}
data class BranchesState(
val branches: List<Ref>,
val isExpanded: Boolean,
val currentBranch: Ref?,
)

View File

@ -1,6 +1,8 @@
package com.jetpackduba.gitnuro.viewmodels
package com.jetpackduba.gitnuro.viewmodels.sidepanel
import com.jetpackduba.gitnuro.exceptions.InvalidRemoteUrlException
import com.jetpackduba.gitnuro.extensions.lowercaseContains
import com.jetpackduba.gitnuro.extensions.simpleName
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.branches.DeleteLocallyRemoteBranchesUseCase
@ -8,18 +10,18 @@ import com.jetpackduba.gitnuro.git.branches.GetRemoteBranchesUseCase
import com.jetpackduba.gitnuro.git.remote_operations.DeleteRemoteBranchUseCase
import com.jetpackduba.gitnuro.git.remotes.*
import com.jetpackduba.gitnuro.ui.dialogs.RemoteWrapper
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RemoteSetUrlCommand
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
class RemotesViewModel @Inject constructor(
class RemotesViewModel @AssistedInject constructor(
private val tabState: TabState,
private val deleteRemoteBranchUseCase: DeleteRemoteBranchUseCase,
private val getRemoteBranchesUseCase: GetRemoteBranchesUseCase,
@ -29,15 +31,37 @@ class RemotesViewModel @Inject constructor(
private val updateRemoteUseCase: UpdateRemoteUseCase,
private val deleteLocallyRemoteBranchesUseCase: DeleteLocallyRemoteBranchesUseCase,
private val tabScope: CoroutineScope,
) : ExpandableViewModel() {
private val _remotes = MutableStateFlow<List<RemoteView>>(listOf())
val remotes: StateFlow<List<RemoteView>>
get() = _remotes
@Assisted
private val filter: StateFlow<String>
) : SidePanelChildViewModel(false) {
private val remotes = MutableStateFlow<List<RemoteView>>(listOf())
val remoteState: StateFlow<RemotesState> = combine(remotes, isExpanded, filter) { remotes, isExpanded, filter ->
val remotesFiltered = remotes.map { remote ->
val remoteInfo = remote.remoteInfo
val newRemoteInfo = remoteInfo.copy(
branchesList = remoteInfo.branchesList.filter { branch ->
branch.simpleName.lowercaseContains(filter)
}
)
remote.copy(remoteInfo = newRemoteInfo)
}
RemotesState(
remotesFiltered,
isExpanded
)
}.stateIn(
scope = tabScope,
started = SharingStarted.Eagerly,
initialValue = RemotesState(emptyList(), isExpanded.value)
)
init {
tabScope.launch {
tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REMOTES)
{
tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REMOTES) {
refresh(tabState.git)
}
}
@ -61,7 +85,7 @@ class RemotesViewModel @Inject constructor(
RemoteView(remoteInfo, true)
}
_remotes.value = remoteViewList
this@RemotesViewModel.remotes.value = remoteViewList
}
fun deleteRemoteBranch(ref: Ref) = tabState.safeProcessing(
@ -74,14 +98,19 @@ class RemotesViewModel @Inject constructor(
loadRemotes(git)
}
fun onRemoteClicked(remoteInfo: RemoteView) {
val remotes = _remotes.value
val newRemoteInfo = remoteInfo.copy(isExpanded = !remoteInfo.isExpanded)
val newRemotesList = remotes.toMutableList()
val indexToReplace = newRemotesList.indexOf(remoteInfo)
newRemotesList[indexToReplace] = newRemoteInfo
fun onRemoteClicked(remoteClicked: RemoteView) {
val remoteName = remoteClicked.remoteInfo.remoteConfig.name
val remotes = this.remotes.value
val remoteInfo = remotes.firstOrNull { it.remoteInfo.remoteConfig.name == remoteName }
_remotes.value = newRemotesList
if(remoteInfo != null) {
val newRemoteInfo = remoteInfo.copy(isExpanded = !remoteClicked.isExpanded)
val newRemotesList = remotes.toMutableList()
val indexToReplace = newRemotesList.indexOf(remoteInfo)
newRemotesList[indexToReplace] = newRemoteInfo
this.remotes.value = newRemotesList
}
}
fun selectBranch(ref: Ref) {
@ -151,4 +180,6 @@ class RemotesViewModel @Inject constructor(
}
}
data class RemoteView(val remoteInfo: RemoteInfo, val isExpanded: Boolean)
data class RemoteView(val remoteInfo: RemoteInfo, val isExpanded: Boolean)
data class RemotesState(val remotes: List<RemoteView>, val isExpanded: Boolean)

View File

@ -1,9 +1,9 @@
package com.jetpackduba.gitnuro.viewmodels
package com.jetpackduba.gitnuro.viewmodels.sidepanel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
abstract class ExpandableViewModel(expandedDefault: Boolean = false) {
abstract class SidePanelChildViewModel(expandedDefault: Boolean) {
private val _isExpanded = MutableStateFlow(expandedDefault)
val isExpanded: StateFlow<Boolean> = _isExpanded

View File

@ -0,0 +1,28 @@
package com.jetpackduba.gitnuro.viewmodels.sidepanel
import com.jetpackduba.gitnuro.di.factories.BranchesViewModelFactory
import com.jetpackduba.gitnuro.di.factories.RemotesViewModelFactory
import com.jetpackduba.gitnuro.di.factories.StashesViewModelFactory
import com.jetpackduba.gitnuro.di.factories.TagsViewModelFactory
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
class SidePanelViewModel @Inject constructor(
branchesViewModelFactory: BranchesViewModelFactory,
remotesViewModelFactory: RemotesViewModelFactory,
tagsViewModelFactory: TagsViewModelFactory,
stashesViewModelFactory: StashesViewModelFactory,
) {
private val _filter = MutableStateFlow("")
val filter: StateFlow<String> = _filter
val branchesViewModel: BranchesViewModel = branchesViewModelFactory.create(filter)
val remotesViewModel: RemotesViewModel = remotesViewModelFactory.create(filter)
val tagsViewModel: TagsViewModel = tagsViewModelFactory.create(filter)
val stashesViewModel: StashesViewModel = stashesViewModelFactory.create(filter)
fun newFilter(newValue: String) {
_filter.value = newValue
}
}

View File

@ -1,5 +1,6 @@
package com.jetpackduba.gitnuro.viewmodels
package com.jetpackduba.gitnuro.viewmodels.sidepanel
import com.jetpackduba.gitnuro.extensions.lowercaseContains
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.stash.ApplyStashUseCase
@ -7,26 +8,36 @@ import com.jetpackduba.gitnuro.git.stash.DeleteStashUseCase
import com.jetpackduba.gitnuro.git.stash.GetStashListUseCase
import com.jetpackduba.gitnuro.git.stash.PopStashUseCase
import com.jetpackduba.gitnuro.ui.SelectedItem
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
class StashesViewModel @Inject constructor(
class StashesViewModel @AssistedInject constructor(
private val getStashListUseCase: GetStashListUseCase,
private val applyStashUseCase: ApplyStashUseCase,
private val popStashUseCase: PopStashUseCase,
private val deleteStashUseCase: DeleteStashUseCase,
private val tabState: TabState,
private val tabScope: CoroutineScope,
) : ExpandableViewModel(true) {
private val _stashStatus = MutableStateFlow<StashStatus>(StashStatus.Loaded(listOf()))
@Assisted
private val filter: StateFlow<String>,
) : SidePanelChildViewModel(true) {
private val stashes = MutableStateFlow<List<RevCommit>>(emptyList())
val stashStatus: StateFlow<StashStatus>
get() = _stashStatus
val stashesState: StateFlow<StashesState> = combine(stashes, isExpanded, filter) { stashes, isExpanded, filter ->
StashesState(
stashes = stashes.filter { it.fullMessage.lowercaseContains(filter) },
isExpanded,
)
}.stateIn(
tabScope,
SharingStarted.Eagerly,
StashesState(emptyList(), isExpanded.value)
)
init {
tabScope.launch {
@ -41,9 +52,8 @@ class StashesViewModel @Inject constructor(
}
suspend fun loadStashes(git: Git) {
_stashStatus.value = StashStatus.Loading
val stashList = getStashListUseCase(git)
_stashStatus.value = StashStatus.Loaded(stashList.toList())
stashes.value = stashList
}
suspend fun refresh(git: Git) {
@ -74,7 +84,7 @@ class StashesViewModel @Inject constructor(
stashDropped(stash)
}
fun selectTab(stash: RevCommit) = tabState.runOperation(
fun selectStash(stash: RevCommit) = tabState.runOperation(
refreshType = RefreshType.NONE,
) {
tabState.newSelectedStash(stash)
@ -94,7 +104,4 @@ class StashesViewModel @Inject constructor(
}
sealed class StashStatus {
object Loading : StashStatus()
data class Loaded(val stashes: List<RevCommit>) : StashStatus()
}
data class StashesState(val stashes: List<RevCommit>, val isExpanded: Boolean)

View File

@ -1,4 +1,4 @@
package com.jetpackduba.gitnuro.viewmodels
package com.jetpackduba.gitnuro.viewmodels.sidepanel
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
@ -19,7 +19,7 @@ class SubmodulesViewModel @Inject constructor(
private val initializeSubmoduleUseCase: InitializeSubmoduleUseCase,
private val updateSubmoduleUseCase: UpdateSubmoduleUseCase,
private val tabScope: CoroutineScope,
) : ExpandableViewModel() {
) : SidePanelChildViewModel(true) {
private val _submodules = MutableStateFlow<List<Pair<String, SubmoduleStatus>>>(listOf())
val submodules: StateFlow<List<Pair<String, SubmoduleStatus>>>
get() = _submodules

View File

@ -1,30 +1,43 @@
package com.jetpackduba.gitnuro.viewmodels
package com.jetpackduba.gitnuro.viewmodels.sidepanel
import com.jetpackduba.gitnuro.extensions.lowercaseContains
import com.jetpackduba.gitnuro.extensions.simpleName
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.branches.CheckoutRefUseCase
import com.jetpackduba.gitnuro.git.tags.DeleteTagUseCase
import com.jetpackduba.gitnuro.git.tags.GetTagsUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
class TagsViewModel @Inject constructor(
class TagsViewModel @AssistedInject constructor(
private val tabState: TabState,
private val getTagsUseCase: GetTagsUseCase,
private val deleteTagUseCase: DeleteTagUseCase,
private val checkoutRefUseCase: CheckoutRefUseCase,
private val tabScope: CoroutineScope,
) : ExpandableViewModel() {
private val _tags = MutableStateFlow<List<Ref>>(listOf())
val tags: StateFlow<List<Ref>>
get() = _tags
@Assisted
private val filter: StateFlow<String>
) : SidePanelChildViewModel(false) {
private val tags = MutableStateFlow<List<Ref>>(listOf())
val tagsState: StateFlow<TagsState> = combine(tags, isExpanded, filter) { tags, isExpanded, filter ->
TagsState(
tags.filter { tag -> tag.simpleName.lowercaseContains(filter) },
isExpanded,
)
}.stateIn(
scope = tabScope,
started = SharingStarted.Eagerly,
initialValue = TagsState(emptyList(), isExpanded.value)
)
init {
tabScope.launch {
@ -38,7 +51,7 @@ class TagsViewModel @Inject constructor(
private suspend fun loadTags(git: Git) = withContext(Dispatchers.IO) {
val tagsList = getTagsUseCase(git)
_tags.value = tagsList
tags.value = tagsList
}
fun checkoutRef(ref: Ref) = tabState.safeProcessing(
@ -60,4 +73,6 @@ class TagsViewModel @Inject constructor(
suspend fun refresh(git: Git) {
loadTags(git)
}
}
}
data class TagsState(val tags: List<Ref>, val isExpanded: Boolean)

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>

After

Width:  |  Height:  |  Size: 391 B