Refactored tab management to its own single file without having to deal with unique IDs or having code to manage the tab scattered around the app
This commit is contained in:
parent
0d91ec747a
commit
a55dd755d7
@ -34,13 +34,10 @@ import com.jetpackduba.gitnuro.theme.AppTheme
|
||||
import com.jetpackduba.gitnuro.theme.Theme
|
||||
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
|
||||
import com.jetpackduba.gitnuro.ui.AppTab
|
||||
import com.jetpackduba.gitnuro.ui.TabsManager
|
||||
import com.jetpackduba.gitnuro.ui.components.RepositoriesTabPanel
|
||||
import com.jetpackduba.gitnuro.ui.components.TabInformation
|
||||
import com.jetpackduba.gitnuro.ui.components.emptyTabInformation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.eclipse.jgit.lib.GpgSigner
|
||||
import java.io.File
|
||||
import java.nio.file.Paths
|
||||
@ -65,16 +62,17 @@ class App {
|
||||
@Inject
|
||||
lateinit var appEnvInfo: AppEnvInfo
|
||||
|
||||
@Inject
|
||||
lateinit var tabsManager: TabsManager
|
||||
|
||||
init {
|
||||
appComponent.inject(this)
|
||||
}
|
||||
|
||||
private val tabsFlow = MutableStateFlow<List<TabInformation>>(emptyList())
|
||||
|
||||
fun start(args: Array<String>) {
|
||||
tabsManager.appComponent = this.appComponent
|
||||
val windowPlacement = appSettings.windowPlacement.toWindowPlacement
|
||||
val dirToOpen = getDirToOpen(args)
|
||||
var defaultSelectedTabKey = 0
|
||||
|
||||
appEnvInfo.isFlatpak = args.contains("--flatpak") // TODO Test this
|
||||
appStateManager.loadRepositoriesTabs()
|
||||
@ -88,12 +86,12 @@ class App {
|
||||
ex.printStackTrace()
|
||||
}
|
||||
|
||||
loadTabs()
|
||||
tabsManager.loadPersistedTabs()
|
||||
|
||||
GpgSigner.setDefault(appGpgSigner)
|
||||
|
||||
if (dirToOpen != null)
|
||||
defaultSelectedTabKey = addDirTab(dirToOpen)
|
||||
addDirTab(dirToOpen)
|
||||
|
||||
application {
|
||||
var isOpen by remember { mutableStateOf(true) }
|
||||
@ -129,7 +127,7 @@ class App {
|
||||
customTheme = customTheme,
|
||||
) {
|
||||
Box(modifier = Modifier.background(MaterialTheme.colors.background)) {
|
||||
AppTabs(defaultSelectedTabKey)
|
||||
AppTabs()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -142,100 +140,46 @@ class App {
|
||||
}
|
||||
}
|
||||
|
||||
private fun addDirTab(dirToOpen: File): Int {
|
||||
var defaultSelectedTabKey = 0
|
||||
|
||||
tabsFlow.update {
|
||||
val newList = it.toMutableList()
|
||||
private fun addDirTab(dirToOpen: File) {
|
||||
val absolutePath = dirToOpen.normalize().absolutePath
|
||||
.removeSuffix(systemSeparator)
|
||||
.removeSuffix("$systemSeparator.git")
|
||||
val newKey = it.count()
|
||||
|
||||
val existingIndex =
|
||||
newList.indexOfFirst { repo -> repo.path?.removeSuffix(systemSeparator) == absolutePath }
|
||||
|
||||
defaultSelectedTabKey = if (existingIndex == -1) {
|
||||
newList.add(newAppTab(key = newKey, path = absolutePath))
|
||||
newKey
|
||||
} else {
|
||||
existingIndex
|
||||
}
|
||||
|
||||
newList
|
||||
}
|
||||
|
||||
return defaultSelectedTabKey
|
||||
}
|
||||
|
||||
private fun loadTabs() {
|
||||
val repositoriesSavedTabs = appStateManager.openRepositoriesPathsTabs
|
||||
var repoTabs = repositoriesSavedTabs.map { repositoryTab ->
|
||||
newAppTab(
|
||||
key = repositoryTab.key,
|
||||
path = repositoryTab.value
|
||||
)
|
||||
}
|
||||
|
||||
if (repoTabs.isEmpty()) {
|
||||
repoTabs = listOf(
|
||||
newAppTab()
|
||||
)
|
||||
}
|
||||
|
||||
tabsFlow.value = repoTabs
|
||||
|
||||
println("After reading prefs, got ${tabsFlow.value.count()} tabs")
|
||||
tabsManager.addNewTabFromPath(absolutePath, true)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun AppTabs(defaultSelectedTabKey: Int) {
|
||||
val tabs by tabsFlow.collectAsState()
|
||||
val tabsInformationList = tabs.sortedBy { it.key }
|
||||
val selectedTabKey = remember { mutableStateOf(defaultSelectedTabKey) }
|
||||
fun AppTabs() {
|
||||
val tabs by tabsManager.tabsFlow.collectAsState()
|
||||
val currentTab = tabsManager.currentTab.collectAsState().value
|
||||
|
||||
if(currentTab != null) {
|
||||
Column(
|
||||
modifier = Modifier.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
Tabs(
|
||||
tabsInformationList = tabsInformationList,
|
||||
selectedTabKey = selectedTabKey,
|
||||
onAddedTab = { tabInfo ->
|
||||
addTab(tabInfo)
|
||||
tabsInformationList = tabs,
|
||||
currentTab = currentTab,
|
||||
onAddedTab = {
|
||||
tabsManager.newTab()
|
||||
},
|
||||
onRemoveTab = { key ->
|
||||
removeTab(key)
|
||||
onCloseTab = { tab ->
|
||||
tabsManager.closeTab(tab)
|
||||
}
|
||||
)
|
||||
|
||||
TabsContent(tabsInformationList, selectedTabKey.value)
|
||||
TabsContent(tabs, currentTab)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeTab(key: Int) = appStateManager.appScope.launch(Dispatchers.IO) {
|
||||
// Stop any running jobs
|
||||
val tabs = tabsFlow.value
|
||||
val tabToRemove = tabs.firstOrNull { it.key == key } ?: return@launch
|
||||
tabToRemove.tabViewModel.dispose()
|
||||
|
||||
// Remove tab from persistent tabs storage
|
||||
appStateManager.repositoryTabRemoved(key)
|
||||
|
||||
// Remove from tabs flow
|
||||
tabsFlow.value = tabsFlow.value.filter { tab -> tab.key != key }
|
||||
}
|
||||
|
||||
fun addTab(tabInformation: TabInformation) = appStateManager.appScope.launch(Dispatchers.IO) {
|
||||
tabsFlow.value = tabsFlow.value.toMutableList().apply { add(tabInformation) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Tabs(
|
||||
selectedTabKey: MutableState<Int>,
|
||||
tabsInformationList: List<TabInformation>,
|
||||
onAddedTab: (TabInformation) -> Unit,
|
||||
onRemoveTab: (Int) -> Unit,
|
||||
currentTab: TabInformation?,
|
||||
onAddedTab: () -> Unit,
|
||||
onCloseTab: (TabInformation) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@ -245,35 +189,15 @@ class App {
|
||||
) {
|
||||
RepositoriesTabPanel(
|
||||
tabs = tabsInformationList,
|
||||
selectedTabKey = selectedTabKey.value,
|
||||
onTabSelected = { newSelectedTabKey ->
|
||||
selectedTabKey.value = newSelectedTabKey
|
||||
currentTab = currentTab,
|
||||
onTabSelected = { selectedTab ->
|
||||
tabsManager.selectTab(selectedTab)
|
||||
},
|
||||
onTabClosed = onRemoveTab
|
||||
) { key ->
|
||||
val newAppTab = newAppTab(
|
||||
key = key
|
||||
onTabClosed = onCloseTab,
|
||||
onAddNewTab = onAddedTab
|
||||
)
|
||||
|
||||
onAddedTab(newAppTab)
|
||||
newAppTab
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun newAppTab(
|
||||
key: Int = 0,
|
||||
tabName: MutableState<String> = mutableStateOf("New tab"),
|
||||
path: String? = null,
|
||||
): TabInformation {
|
||||
|
||||
return TabInformation(
|
||||
tabName = tabName,
|
||||
key = key,
|
||||
path = path,
|
||||
appComponent = appComponent,
|
||||
)
|
||||
}
|
||||
|
||||
fun getDirToOpen(args: Array<String>): File? {
|
||||
if (args.isNotEmpty()) {
|
||||
@ -296,20 +220,18 @@ class App {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TabsContent(tabs: List<TabInformation>, selectedTabKey: Int) {
|
||||
val selectedTab = tabs.firstOrNull { it.key == selectedTabKey }
|
||||
|
||||
private fun TabsContent(tabs: List<TabInformation>, currentTab: TabInformation?) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.background)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
if (selectedTab != null) {
|
||||
val density = arrayOf(LocalTabScope provides selectedTab)
|
||||
if (currentTab != null) {
|
||||
val density = arrayOf(LocalTabScope provides currentTab)
|
||||
|
||||
|
||||
CompositionLocalProvider(values = density) {
|
||||
AppTab(selectedTab.tabViewModel)
|
||||
AppTab(currentTab.tabViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,12 +51,6 @@ class AppStateManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun repositoryTabRemoved(key: Int) = appScope.launch(Dispatchers.IO) {
|
||||
_openRepositoriesPaths.remove(key)
|
||||
|
||||
updateSavedRepositoryTabs()
|
||||
}
|
||||
|
||||
private suspend fun updateSavedRepositoryTabs() = withContext(Dispatchers.IO) {
|
||||
val tabsList = _openRepositoriesPaths.map { it.value }
|
||||
appSettings.latestTabsOpened = Json.encodeToString(tabsList)
|
||||
@ -67,16 +61,6 @@ class AppStateManager @Inject constructor(
|
||||
}
|
||||
|
||||
fun loadRepositoriesTabs() {
|
||||
val repositoriesSaved = appSettings.latestTabsOpened
|
||||
|
||||
if (repositoriesSaved.isNotEmpty()) {
|
||||
val repositoriesList = Json.decodeFromString<List<String>>(repositoriesSaved)
|
||||
|
||||
repositoriesList.forEachIndexed { index, repository ->
|
||||
_openRepositoriesPaths[index] = repository
|
||||
}
|
||||
}
|
||||
|
||||
val repositoriesPathsSaved = appSettings.latestOpenedRepositoriesPath
|
||||
if (repositoriesPathsSaved.isNotEmpty()) {
|
||||
val repositories = Json.decodeFromString<List<String>>(repositoriesPathsSaved)
|
||||
|
128
src/main/kotlin/com/jetpackduba/gitnuro/ui/TabsManager.kt
Normal file
128
src/main/kotlin/com/jetpackduba/gitnuro/ui/TabsManager.kt
Normal file
@ -0,0 +1,128 @@
|
||||
package com.jetpackduba.gitnuro.ui
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.jetpackduba.gitnuro.di.AppComponent
|
||||
import com.jetpackduba.gitnuro.preferences.AppSettings
|
||||
import com.jetpackduba.gitnuro.ui.components.TabInformation
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class TabsManager @Inject constructor(
|
||||
private val appSettings: AppSettings
|
||||
) {
|
||||
lateinit var appComponent: AppComponent
|
||||
|
||||
private val _tabsFlow = MutableStateFlow<List<TabInformation>>(emptyList())
|
||||
val tabsFlow: StateFlow<List<TabInformation>> = _tabsFlow
|
||||
|
||||
private val _currentTab = MutableStateFlow<TabInformation?>(null)
|
||||
val currentTab: StateFlow<TabInformation?> = _currentTab
|
||||
|
||||
fun loadPersistedTabs() {
|
||||
val repositoriesSaved = appSettings.latestTabsOpened
|
||||
|
||||
val tabs = if (repositoriesSaved.isNotEmpty()) {
|
||||
val repositoriesList = Json.decodeFromString<List<String>>(repositoriesSaved)
|
||||
|
||||
repositoriesList.map { path ->
|
||||
newAppTab(
|
||||
path = path,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
listOf(newAppTab())
|
||||
}
|
||||
|
||||
_tabsFlow.value = tabs
|
||||
_currentTab.value = _tabsFlow.value.first()
|
||||
}
|
||||
|
||||
fun addNewTabFromPath(path: String, selectTab: Boolean) {
|
||||
val newTab = newAppTab(
|
||||
tabName = mutableStateOf(""),
|
||||
path = path,
|
||||
)
|
||||
|
||||
_tabsFlow.update {
|
||||
it.toMutableList().apply {
|
||||
add(newTab)
|
||||
}
|
||||
}
|
||||
|
||||
if (selectTab) {
|
||||
_currentTab.value = newTab
|
||||
}
|
||||
}
|
||||
|
||||
fun selectTab(tab: TabInformation) {
|
||||
_currentTab.value = tab
|
||||
}
|
||||
|
||||
fun closeTab(tab: TabInformation) {
|
||||
val tabsList = _tabsFlow.value.toMutableList()
|
||||
var newCurrentTab: TabInformation? = null
|
||||
|
||||
if (currentTab.value == tab) {
|
||||
val index = tabsList.indexOf(tab)
|
||||
if (tabsList.count() == 1) {
|
||||
newCurrentTab = newAppTab()
|
||||
} else if (index > 0) {
|
||||
newCurrentTab = tabsList[index - 1]
|
||||
} else if (index == 0) {
|
||||
newCurrentTab = tabsList[1]
|
||||
}
|
||||
}
|
||||
|
||||
tab.tabViewModel.dispose()
|
||||
tabsList.remove(tab)
|
||||
|
||||
if (newCurrentTab != null) {
|
||||
if (!tabsList.contains(newCurrentTab)) {
|
||||
tabsList.add(newCurrentTab)
|
||||
}
|
||||
|
||||
_tabsFlow.value = tabsList
|
||||
_currentTab.value = newCurrentTab
|
||||
} else {
|
||||
_tabsFlow.value = tabsList
|
||||
}
|
||||
|
||||
updatePersistedTabs()
|
||||
}
|
||||
|
||||
private fun updatePersistedTabs() {
|
||||
val tabs = tabsFlow.value
|
||||
appSettings.latestTabsOpened = Json.encodeToString(tabs)
|
||||
}
|
||||
|
||||
fun newTab() {
|
||||
val newTab = newAppTab()
|
||||
|
||||
_tabsFlow.update {
|
||||
it.toMutableList().apply {
|
||||
add(newTab)
|
||||
}
|
||||
}
|
||||
|
||||
_currentTab.value = newTab
|
||||
}
|
||||
|
||||
private fun newAppTab(
|
||||
tabName: MutableState<String> = mutableStateOf("New tab"),
|
||||
path: String? = null,
|
||||
): TabInformation {
|
||||
return TabInformation(
|
||||
tabName = tabName,
|
||||
path = path,
|
||||
appComponent = appComponent,
|
||||
)
|
||||
}
|
||||
}
|
@ -22,17 +22,13 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.jetpackduba.gitnuro.App
|
||||
import com.jetpackduba.gitnuro.AppStateManager
|
||||
import com.jetpackduba.gitnuro.LocalTabScope
|
||||
import com.jetpackduba.gitnuro.credentials.CredentialsStateManager
|
||||
import com.jetpackduba.gitnuro.di.AppComponent
|
||||
import com.jetpackduba.gitnuro.di.DaggerTabComponent
|
||||
import com.jetpackduba.gitnuro.di.TabComponent
|
||||
import com.jetpackduba.gitnuro.extensions.handMouseClickable
|
||||
import com.jetpackduba.gitnuro.extensions.handOnHover
|
||||
import com.jetpackduba.gitnuro.preferences.AppSettings
|
||||
import com.jetpackduba.gitnuro.viewmodels.SettingsViewModel
|
||||
import com.jetpackduba.gitnuro.viewmodels.TabViewModel
|
||||
import com.jetpackduba.gitnuro.viewmodels.TabViewModelsHolder
|
||||
import javax.inject.Inject
|
||||
@ -42,21 +38,20 @@ import kotlin.io.path.name
|
||||
@Composable
|
||||
fun RepositoriesTabPanel(
|
||||
tabs: List<TabInformation>,
|
||||
selectedTabKey: Int,
|
||||
onTabSelected: (Int) -> Unit,
|
||||
onTabClosed: (Int) -> Unit,
|
||||
newTabContent: (key: Int) -> TabInformation,
|
||||
currentTab: TabInformation?,
|
||||
onTabSelected: (TabInformation) -> Unit,
|
||||
onTabClosed: (TabInformation) -> Unit,
|
||||
onAddNewTab: () -> Unit,
|
||||
) {
|
||||
var tabsIdentifier by remember { mutableStateOf(tabs.count()) }
|
||||
val stateHorizontal = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(selectedTabKey) {
|
||||
val index = tabs.indexOfFirst { it.key == selectedTabKey }
|
||||
// todo sometimes it scrolls to (index - 1) for some weird reason
|
||||
if (index > -1) {
|
||||
stateHorizontal.scrollToItem(index)
|
||||
}
|
||||
}
|
||||
// LaunchedEffect(selectedTabKey) {
|
||||
// val index = tabs.indexOfFirst { it.key == selectedTabKey }
|
||||
// // todo sometimes it scrolls to (index - 1) for some weird reason
|
||||
// if (index > -1) {
|
||||
// stateHorizontal.scrollToItem(index)
|
||||
// }
|
||||
// }
|
||||
|
||||
val canBeScrolled by remember {
|
||||
derivedStateOf {
|
||||
@ -89,31 +84,15 @@ fun RepositoriesTabPanel(
|
||||
.fillMaxHeight(),
|
||||
state = stateHorizontal,
|
||||
) {
|
||||
items(items = tabs, key = { it.key }) { tab ->
|
||||
items(items = tabs) { tab ->
|
||||
Tab(
|
||||
title = tab.tabName,
|
||||
isSelected = tab.key == selectedTabKey,
|
||||
isSelected = currentTab == tab,
|
||||
onClick = {
|
||||
onTabSelected(tab.key)
|
||||
onTabSelected(tab)
|
||||
},
|
||||
onCloseTab = {
|
||||
val isTabSelected = selectedTabKey == tab.key
|
||||
|
||||
if (isTabSelected) {
|
||||
val nextKey = getTabNextKey(tab, tabs)
|
||||
|
||||
if (nextKey >= 0) {
|
||||
onTabSelected(nextKey)
|
||||
} else {
|
||||
tabsIdentifier++
|
||||
|
||||
// Create a new tab if the tabs list is empty after removing the current one
|
||||
newTabContent(tabsIdentifier)
|
||||
onTabSelected(tabsIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
onTabClosed(tab.key)
|
||||
onTabClosed(tab)
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -135,10 +114,7 @@ fun RepositoriesTabPanel(
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
tabsIdentifier++
|
||||
|
||||
newTabContent(tabsIdentifier)
|
||||
onTabSelected(tabsIdentifier)
|
||||
onAddNewTab()
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
@ -154,24 +130,6 @@ fun RepositoriesTabPanel(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getTabNextKey(tab: TabInformation, tabs: List<TabInformation>): Int {
|
||||
val index = tabs.indexOf(tab)
|
||||
val nextIndex = if (index == 0 && tabs.count() >= 2) {
|
||||
1 // If the first tab is selected, select the next one
|
||||
} else if (index == tabs.count() - 1 && tabs.count() >= 2)
|
||||
index - 1 // If the last tab is selected, select the previous one
|
||||
else if (tabs.count() >= 2)
|
||||
index + 1 // If any in between tab is selected, select the next one
|
||||
else
|
||||
-1 // If there aren't any additional tabs once we remove this one
|
||||
|
||||
return if (nextIndex >= 0)
|
||||
tabs[nextIndex].key
|
||||
else
|
||||
-1
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Tab(title: MutableState<String>, isSelected: Boolean, onClick: () -> Unit, onCloseTab: () -> Unit) {
|
||||
Box {
|
||||
@ -239,7 +197,6 @@ fun Tab(title: MutableState<String>, isSelected: Boolean, onClick: () -> Unit, o
|
||||
|
||||
class TabInformation(
|
||||
val tabName: MutableState<String>,
|
||||
val key: Int,
|
||||
val path: String?,
|
||||
appComponent: AppComponent?,
|
||||
) {
|
||||
@ -261,10 +218,10 @@ class TabInformation(
|
||||
|
||||
tabViewModel.onRepositoryChanged = { path ->
|
||||
if (path == null) {
|
||||
appStateManager.repositoryTabRemoved(key)
|
||||
// appStateManager.repositoryTabRemoved(key)
|
||||
} else {
|
||||
tabName.value = Path(path).name
|
||||
appStateManager.repositoryTabChanged(key, path)
|
||||
// appStateManager.repositoryTabChanged(path)
|
||||
}
|
||||
}
|
||||
if (path != null)
|
||||
@ -272,7 +229,7 @@ class TabInformation(
|
||||
}
|
||||
}
|
||||
|
||||
fun emptyTabInformation() = TabInformation(mutableStateOf(""), 0, "", null)
|
||||
fun emptyTabInformation() = TabInformation(mutableStateOf(""), "", null)
|
||||
|
||||
@Composable
|
||||
inline fun <reified T> gitnuroViewModel(): T {
|
||||
|
Loading…
Reference in New Issue
Block a user