From a55dd755d7cd38fd4812d5fd21e2a96ad9331067 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Wed, 5 Apr 2023 02:10:17 +0200 Subject: [PATCH] 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 --- .../kotlin/com/jetpackduba/gitnuro/App.kt | 168 +++++------------- .../jetpackduba/gitnuro/AppStateManager.kt | 16 -- .../com/jetpackduba/gitnuro/ui/TabsManager.kt | 128 +++++++++++++ .../ui/components/RepositoriesTabPanel.kt | 81 ++------- 4 files changed, 192 insertions(+), 201 deletions(-) create mode 100644 src/main/kotlin/com/jetpackduba/gitnuro/ui/TabsManager.kt diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/App.kt b/src/main/kotlin/com/jetpackduba/gitnuro/App.kt index 8469062..9df4119 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/App.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/App.kt @@ -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>(emptyList()) - fun start(args: Array) { + 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 + private fun addDirTab(dirToOpen: File) { + val absolutePath = dirToOpen.normalize().absolutePath + .removeSuffix(systemSeparator) + .removeSuffix("$systemSeparator.git") - tabsFlow.update { - val newList = it.toMutableList() - 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 - Column( - modifier = Modifier.background(MaterialTheme.colors.background) - ) { - Tabs( - tabsInformationList = tabsInformationList, - selectedTabKey = selectedTabKey, - onAddedTab = { tabInfo -> - addTab(tabInfo) - }, - onRemoveTab = { key -> - removeTab(key) - } - ) + if(currentTab != null) { + Column( + modifier = Modifier.background(MaterialTheme.colors.background) + ) { + Tabs( + tabsInformationList = tabs, + currentTab = currentTab, + onAddedTab = { + tabsManager.newTab() + }, + 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, tabsInformationList: List, - onAddedTab: (TabInformation) -> Unit, - onRemoveTab: (Int) -> Unit, + currentTab: TabInformation?, + onAddedTab: () -> Unit, + onCloseTab: (TabInformation) -> Unit, ) { Row( modifier = Modifier @@ -245,36 +189,16 @@ 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 - ) - - onAddedTab(newAppTab) - newAppTab - } + onTabClosed = onCloseTab, + onAddNewTab = onAddedTab + ) } } - private fun newAppTab( - key: Int = 0, - tabName: MutableState = mutableStateOf("New tab"), - path: String? = null, - ): TabInformation { - - return TabInformation( - tabName = tabName, - key = key, - path = path, - appComponent = appComponent, - ) - } - fun getDirToOpen(args: Array): File? { if (args.isNotEmpty()) { val repoToOpen = args.first() @@ -296,20 +220,18 @@ class App { } @Composable -private fun TabsContent(tabs: List, selectedTabKey: Int) { - val selectedTab = tabs.firstOrNull { it.key == selectedTabKey } - +private fun TabsContent(tabs: List, 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) } } } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/AppStateManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/AppStateManager.kt index 7bde386..f15abc9 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/AppStateManager.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/AppStateManager.kt @@ -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>(repositoriesSaved) - - repositoriesList.forEachIndexed { index, repository -> - _openRepositoriesPaths[index] = repository - } - } - val repositoriesPathsSaved = appSettings.latestOpenedRepositoriesPath if (repositoriesPathsSaved.isNotEmpty()) { val repositories = Json.decodeFromString>(repositoriesPathsSaved) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/TabsManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/TabsManager.kt new file mode 100644 index 0000000..a934711 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/TabsManager.kt @@ -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>(emptyList()) + val tabsFlow: StateFlow> = _tabsFlow + + private val _currentTab = MutableStateFlow(null) + val currentTab: StateFlow = _currentTab + + fun loadPersistedTabs() { + val repositoriesSaved = appSettings.latestTabsOpened + + val tabs = if (repositoriesSaved.isNotEmpty()) { + val repositoriesList = Json.decodeFromString>(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 = mutableStateOf("New tab"), + path: String? = null, + ): TabInformation { + return TabInformation( + tabName = tabName, + path = path, + appComponent = appComponent, + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt index c702d2c..5bbe928 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt @@ -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, - 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): 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, isSelected: Boolean, onClick: () -> Unit, onCloseTab: () -> Unit) { Box { @@ -239,7 +197,6 @@ fun Tab(title: MutableState, isSelected: Boolean, onClick: () -> Unit, o class TabInformation( val tabName: MutableState, - 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 gitnuroViewModel(): T {