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.Theme
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
import com.jetpackduba.gitnuro.ui.AppTab 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.RepositoriesTabPanel
import com.jetpackduba.gitnuro.ui.components.TabInformation import com.jetpackduba.gitnuro.ui.components.TabInformation
import com.jetpackduba.gitnuro.ui.components.emptyTabInformation 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 org.eclipse.jgit.lib.GpgSigner
import java.io.File import java.io.File
import java.nio.file.Paths import java.nio.file.Paths
@ -65,16 +62,17 @@ class App {
@Inject @Inject
lateinit var appEnvInfo: AppEnvInfo lateinit var appEnvInfo: AppEnvInfo
@Inject
lateinit var tabsManager: TabsManager
init { init {
appComponent.inject(this) appComponent.inject(this)
} }
private val tabsFlow = MutableStateFlow<List<TabInformation>>(emptyList())
fun start(args: Array<String>) { fun start(args: Array<String>) {
tabsManager.appComponent = this.appComponent
val windowPlacement = appSettings.windowPlacement.toWindowPlacement val windowPlacement = appSettings.windowPlacement.toWindowPlacement
val dirToOpen = getDirToOpen(args) val dirToOpen = getDirToOpen(args)
var defaultSelectedTabKey = 0
appEnvInfo.isFlatpak = args.contains("--flatpak") // TODO Test this appEnvInfo.isFlatpak = args.contains("--flatpak") // TODO Test this
appStateManager.loadRepositoriesTabs() appStateManager.loadRepositoriesTabs()
@ -88,12 +86,12 @@ class App {
ex.printStackTrace() ex.printStackTrace()
} }
loadTabs() tabsManager.loadPersistedTabs()
GpgSigner.setDefault(appGpgSigner) GpgSigner.setDefault(appGpgSigner)
if (dirToOpen != null) if (dirToOpen != null)
defaultSelectedTabKey = addDirTab(dirToOpen) addDirTab(dirToOpen)
application { application {
var isOpen by remember { mutableStateOf(true) } var isOpen by remember { mutableStateOf(true) }
@ -129,7 +127,7 @@ class App {
customTheme = customTheme, customTheme = customTheme,
) { ) {
Box(modifier = Modifier.background(MaterialTheme.colors.background)) { Box(modifier = Modifier.background(MaterialTheme.colors.background)) {
AppTabs(defaultSelectedTabKey) AppTabs()
} }
} }
} }
@ -142,100 +140,46 @@ class App {
} }
} }
private fun addDirTab(dirToOpen: File): Int { private fun addDirTab(dirToOpen: File) {
var defaultSelectedTabKey = 0 val absolutePath = dirToOpen.normalize().absolutePath
.removeSuffix(systemSeparator)
.removeSuffix("$systemSeparator.git")
tabsFlow.update { tabsManager.addNewTabFromPath(absolutePath, true)
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")
} }
@Composable @Composable
fun AppTabs(defaultSelectedTabKey: Int) { fun AppTabs() {
val tabs by tabsFlow.collectAsState() val tabs by tabsManager.tabsFlow.collectAsState()
val tabsInformationList = tabs.sortedBy { it.key } val currentTab = tabsManager.currentTab.collectAsState().value
val selectedTabKey = remember { mutableStateOf(defaultSelectedTabKey) }
Column( if(currentTab != null) {
modifier = Modifier.background(MaterialTheme.colors.background) Column(
) { modifier = Modifier.background(MaterialTheme.colors.background)
Tabs( ) {
tabsInformationList = tabsInformationList, Tabs(
selectedTabKey = selectedTabKey, tabsInformationList = tabs,
onAddedTab = { tabInfo -> currentTab = currentTab,
addTab(tabInfo) 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 @Composable
fun Tabs( fun Tabs(
selectedTabKey: MutableState<Int>,
tabsInformationList: List<TabInformation>, tabsInformationList: List<TabInformation>,
onAddedTab: (TabInformation) -> Unit, currentTab: TabInformation?,
onRemoveTab: (Int) -> Unit, onAddedTab: () -> Unit,
onCloseTab: (TabInformation) -> Unit,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -245,36 +189,16 @@ class App {
) { ) {
RepositoriesTabPanel( RepositoriesTabPanel(
tabs = tabsInformationList, tabs = tabsInformationList,
selectedTabKey = selectedTabKey.value, currentTab = currentTab,
onTabSelected = { newSelectedTabKey -> onTabSelected = { selectedTab ->
selectedTabKey.value = newSelectedTabKey tabsManager.selectTab(selectedTab)
}, },
onTabClosed = onRemoveTab onTabClosed = onCloseTab,
) { key -> onAddNewTab = onAddedTab
val newAppTab = newAppTab( )
key = key
)
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? { fun getDirToOpen(args: Array<String>): File? {
if (args.isNotEmpty()) { if (args.isNotEmpty()) {
val repoToOpen = args.first() val repoToOpen = args.first()
@ -296,20 +220,18 @@ class App {
} }
@Composable @Composable
private fun TabsContent(tabs: List<TabInformation>, selectedTabKey: Int) { private fun TabsContent(tabs: List<TabInformation>, currentTab: TabInformation?) {
val selectedTab = tabs.firstOrNull { it.key == selectedTabKey }
Box( Box(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
.fillMaxSize(), .fillMaxSize(),
) { ) {
if (selectedTab != null) { if (currentTab != null) {
val density = arrayOf(LocalTabScope provides selectedTab) val density = arrayOf(LocalTabScope provides currentTab)
CompositionLocalProvider(values = density) { 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) { private suspend fun updateSavedRepositoryTabs() = withContext(Dispatchers.IO) {
val tabsList = _openRepositoriesPaths.map { it.value } val tabsList = _openRepositoriesPaths.map { it.value }
appSettings.latestTabsOpened = Json.encodeToString(tabsList) appSettings.latestTabsOpened = Json.encodeToString(tabsList)
@ -67,16 +61,6 @@ class AppStateManager @Inject constructor(
} }
fun loadRepositoriesTabs() { 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 val repositoriesPathsSaved = appSettings.latestOpenedRepositoriesPath
if (repositoriesPathsSaved.isNotEmpty()) { if (repositoriesPathsSaved.isNotEmpty()) {
val repositories = Json.decodeFromString<List<String>>(repositoriesPathsSaved) 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.Modifier
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.App
import com.jetpackduba.gitnuro.AppStateManager import com.jetpackduba.gitnuro.AppStateManager
import com.jetpackduba.gitnuro.LocalTabScope import com.jetpackduba.gitnuro.LocalTabScope
import com.jetpackduba.gitnuro.credentials.CredentialsStateManager
import com.jetpackduba.gitnuro.di.AppComponent import com.jetpackduba.gitnuro.di.AppComponent
import com.jetpackduba.gitnuro.di.DaggerTabComponent import com.jetpackduba.gitnuro.di.DaggerTabComponent
import com.jetpackduba.gitnuro.di.TabComponent import com.jetpackduba.gitnuro.di.TabComponent
import com.jetpackduba.gitnuro.extensions.handMouseClickable import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.extensions.handOnHover 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.TabViewModel
import com.jetpackduba.gitnuro.viewmodels.TabViewModelsHolder import com.jetpackduba.gitnuro.viewmodels.TabViewModelsHolder
import javax.inject.Inject import javax.inject.Inject
@ -42,21 +38,20 @@ import kotlin.io.path.name
@Composable @Composable
fun RepositoriesTabPanel( fun RepositoriesTabPanel(
tabs: List<TabInformation>, tabs: List<TabInformation>,
selectedTabKey: Int, currentTab: TabInformation?,
onTabSelected: (Int) -> Unit, onTabSelected: (TabInformation) -> Unit,
onTabClosed: (Int) -> Unit, onTabClosed: (TabInformation) -> Unit,
newTabContent: (key: Int) -> TabInformation, onAddNewTab: () -> Unit,
) { ) {
var tabsIdentifier by remember { mutableStateOf(tabs.count()) }
val stateHorizontal = rememberLazyListState() val stateHorizontal = rememberLazyListState()
LaunchedEffect(selectedTabKey) { // LaunchedEffect(selectedTabKey) {
val index = tabs.indexOfFirst { it.key == selectedTabKey } // val index = tabs.indexOfFirst { it.key == selectedTabKey }
// todo sometimes it scrolls to (index - 1) for some weird reason // // todo sometimes it scrolls to (index - 1) for some weird reason
if (index > -1) { // if (index > -1) {
stateHorizontal.scrollToItem(index) // stateHorizontal.scrollToItem(index)
} // }
} // }
val canBeScrolled by remember { val canBeScrolled by remember {
derivedStateOf { derivedStateOf {
@ -89,31 +84,15 @@ fun RepositoriesTabPanel(
.fillMaxHeight(), .fillMaxHeight(),
state = stateHorizontal, state = stateHorizontal,
) { ) {
items(items = tabs, key = { it.key }) { tab -> items(items = tabs) { tab ->
Tab( Tab(
title = tab.tabName, title = tab.tabName,
isSelected = tab.key == selectedTabKey, isSelected = currentTab == tab,
onClick = { onClick = {
onTabSelected(tab.key) onTabSelected(tab)
}, },
onCloseTab = { onCloseTab = {
val isTabSelected = selectedTabKey == tab.key onTabClosed(tab)
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)
} }
) )
} }
@ -135,10 +114,7 @@ fun RepositoriesTabPanel(
IconButton( IconButton(
onClick = { onClick = {
tabsIdentifier++ onAddNewTab()
newTabContent(tabsIdentifier)
onTabSelected(tabsIdentifier)
}, },
modifier = Modifier modifier = Modifier
.size(36.dp) .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 @Composable
fun Tab(title: MutableState<String>, isSelected: Boolean, onClick: () -> Unit, onCloseTab: () -> Unit) { fun Tab(title: MutableState<String>, isSelected: Boolean, onClick: () -> Unit, onCloseTab: () -> Unit) {
Box { Box {
@ -239,7 +197,6 @@ fun Tab(title: MutableState<String>, isSelected: Boolean, onClick: () -> Unit, o
class TabInformation( class TabInformation(
val tabName: MutableState<String>, val tabName: MutableState<String>,
val key: Int,
val path: String?, val path: String?,
appComponent: AppComponent?, appComponent: AppComponent?,
) { ) {
@ -261,10 +218,10 @@ class TabInformation(
tabViewModel.onRepositoryChanged = { path -> tabViewModel.onRepositoryChanged = { path ->
if (path == null) { if (path == null) {
appStateManager.repositoryTabRemoved(key) // appStateManager.repositoryTabRemoved(key)
} else { } else {
tabName.value = Path(path).name tabName.value = Path(path).name
appStateManager.repositoryTabChanged(key, path) // appStateManager.repositoryTabChanged(path)
} }
} }
if (path != null) if (path != null)
@ -272,7 +229,7 @@ class TabInformation(
} }
} }
fun emptyTabInformation() = TabInformation(mutableStateOf(""), 0, "", null) fun emptyTabInformation() = TabInformation(mutableStateOf(""), "", null)
@Composable @Composable
inline fun <reified T> gitnuroViewModel(): T { inline fun <reified T> gitnuroViewModel(): T {