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:
Abdelilah El Aissaoui 2023-04-05 02:10:17 +02:00
parent 0d91ec747a
commit a55dd755d7
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
4 changed files with 192 additions and 201 deletions

View File

@ -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)
}
}
}

View File

@ -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)

View 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,
)
}
}

View File

@ -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 {