Recent repositories now are unlimited, can be removed and searched

This commit is contained in:
Abdelilah El Aissaoui 2024-06-16 03:04:08 +02:00
parent ef41e9acf0
commit 127f2158ee
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
8 changed files with 283 additions and 180 deletions

View File

@ -5,6 +5,8 @@ import com.jetpackduba.gitnuro.repositories.AppSettingsRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.decodeFromString
@ -20,26 +22,25 @@ class AppStateManager @Inject constructor(
) {
private val mutex = Mutex()
private val _latestOpenedRepositoriesPaths = mutableListOf<String>()
val latestOpenedRepositoriesPaths: List<String>
get() = _latestOpenedRepositoriesPaths
private val _latestOpenedRepositoriesPaths = MutableStateFlow<List<String>>(emptyList())
val latestOpenedRepositoriesPaths = _latestOpenedRepositoriesPaths.asStateFlow()
val latestOpenedRepositoryPath: String
get() = _latestOpenedRepositoriesPaths.firstOrNull() ?: ""
get() = _latestOpenedRepositoriesPaths.value.firstOrNull() ?: ""
fun repositoryTabChanged(path: String) = appScope.launch(Dispatchers.IO) {
mutex.lock()
try {
val repoPaths = _latestOpenedRepositoriesPaths.value.toMutableList()
// Remove any previously existing path
_latestOpenedRepositoriesPaths.removeIf { it == path }
repoPaths.removeIf { it == path }
// Add the latest one to the beginning
_latestOpenedRepositoriesPaths.add(0, path)
repoPaths.add(0, path)
if (_latestOpenedRepositoriesPaths.count() > 10)
_latestOpenedRepositoriesPaths.removeLast()
appSettingsRepository.latestOpenedRepositoriesPath = Json.encodeToString(_latestOpenedRepositoriesPaths)
appSettingsRepository.latestOpenedRepositoriesPath = Json.encodeToString(repoPaths)
_latestOpenedRepositoriesPaths.value = repoPaths
} finally {
mutex.unlock()
}
@ -49,11 +50,28 @@ class AppStateManager @Inject constructor(
val repositoriesPathsSaved = appSettingsRepository.latestOpenedRepositoriesPath
if (repositoriesPathsSaved.isNotEmpty()) {
val repositories = Json.decodeFromString<List<String>>(repositoriesPathsSaved)
_latestOpenedRepositoriesPaths.addAll(repositories)
val repoPaths = _latestOpenedRepositoriesPaths.value.toMutableList()
repoPaths.addAll(repositories)
_latestOpenedRepositoriesPaths.value = repoPaths
}
}
fun cancelCoroutines() {
appScope.cancel("Closing app")
}
fun removeRepositoryFromRecent(path: String) = appScope.launch {
mutex.lock()
try {
val repoPaths = _latestOpenedRepositoriesPaths.value.toMutableList()
repoPaths.removeIf { it == path }
appSettingsRepository.latestOpenedRepositoriesPath = Json.encodeToString(repoPaths)
_latestOpenedRepositoriesPaths.value = repoPaths
} finally {
mutex.unlock()
}
}
}

View File

@ -21,11 +21,13 @@ import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.git.rebase.RebaseInteractiveState
import com.jetpackduba.gitnuro.keybindings.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.models.AuthorInfoSimple
import com.jetpackduba.gitnuro.ui.components.SecondaryButton
import com.jetpackduba.gitnuro.ui.components.TripleVerticalSplitPanel
import com.jetpackduba.gitnuro.ui.dialogs.*
import com.jetpackduba.gitnuro.ui.diff.Diff
import com.jetpackduba.gitnuro.ui.log.Log
import com.jetpackduba.gitnuro.updates.Update
import com.jetpackduba.gitnuro.viewmodels.BlameState
import com.jetpackduba.gitnuro.viewmodels.TabViewModel
import org.eclipse.jgit.lib.RepositoryState
@ -160,14 +162,26 @@ fun RepositoryOpenPage(
.background(MaterialTheme.colors.primaryVariant.copy(alpha = 0.2f))
)
BottomInfoBar(tabViewModel)
val userInfo by tabViewModel.authorInfoSimple.collectAsState()
val newUpdate = tabViewModel.update.collectAsState().value
BottomInfoBar(
userInfo,
newUpdate,
onOpenUrlInBrowser = { tabViewModel.openUrlInBrowser(it) },
onShowAuthorInfoDialog = { tabViewModel.showAuthorInfoDialog() },
)
}
}
@Composable
private fun BottomInfoBar(tabViewModel: TabViewModel) {
val userInfo by tabViewModel.authorInfoSimple.collectAsState()
val newUpdate = tabViewModel.hasUpdates.collectAsState().value
private fun BottomInfoBar(
userInfo: AuthorInfoSimple,
newUpdate: Update?,
onOpenUrlInBrowser: (String) -> Unit,
onShowAuthorInfoDialog: () -> Unit,
) {
Row(
modifier = Modifier
@ -180,7 +194,7 @@ private fun BottomInfoBar(tabViewModel: TabViewModel) {
Box(
modifier = Modifier
.fillMaxHeight()
.handMouseClickable { tabViewModel.showAuthorInfoDialog() },
.handMouseClickable { onShowAuthorInfoDialog() },
contentAlignment = Alignment.Center,
) {
Text(
@ -194,7 +208,7 @@ private fun BottomInfoBar(tabViewModel: TabViewModel) {
if (newUpdate != null) {
SecondaryButton(
text = "Update ${newUpdate.appVersion} available",
onClick = { tabViewModel.openUrlInBrowser(newUpdate.downloadUrl) },
onClick = { onOpenUrlInBrowser(newUpdate.downloadUrl) },
backgroundButton = MaterialTheme.colors.primary,
modifier = Modifier.padding(end = 16.dp)
)

View File

@ -2,11 +2,14 @@
package com.jetpackduba.gitnuro.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
@ -14,6 +17,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
@ -24,15 +28,14 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.AppConstants
import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.extensions.dirName
import com.jetpackduba.gitnuro.extensions.dirPath
import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.extensions.handOnHover
import com.jetpackduba.gitnuro.managers.AppStateManager
import com.jetpackduba.gitnuro.extensions.*
import com.jetpackduba.gitnuro.theme.AppTheme
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
import com.jetpackduba.gitnuro.theme.textButtonColors
import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField
import com.jetpackduba.gitnuro.ui.components.SecondaryButton
import com.jetpackduba.gitnuro.ui.dialogs.AppInfoDialog
import com.jetpackduba.gitnuro.updates.Update
import com.jetpackduba.gitnuro.viewmodels.TabViewModel
@ -42,44 +45,113 @@ fun WelcomePage(
onShowCloneDialog: () -> Unit,
onShowSettings: () -> Unit,
) {
val appStateManager = tabViewModel.appStateManager
var showAdditionalInfo by remember { mutableStateOf(false) }
val recentlyOpenedRepositories by tabViewModel.appStateManager.latestOpenedRepositoriesPaths.collectAsState()
val newUpdate by tabViewModel.update.collectAsState()
WelcomeView(
recentlyOpenedRepositories,
newUpdate,
onShowCloneDialog = onShowCloneDialog,
onShowSettings = onShowSettings,
onOpenRepository = {
val repo = tabViewModel.openDirectoryPicker()
if (repo != null) {
tabViewModel.openRepository(repo)
}
},
onStartRepository = {
val dir = tabViewModel.openDirectoryPicker()
if (dir != null) {
tabViewModel.initLocalRepository(dir)
}
},
onOpenKnownRepository = { tabViewModel.openRepository(it) },
onOpenUrlInBrowser = { tabViewModel.openUrlInBrowser(it) },
onRemoveRepositoryFromRecent = { tabViewModel.removeRepositoryFromRecent(it) }
)
}
@Preview
@Composable
fun WelcomeViewPreview() {
AppTheme(
customTheme = null
) {
val recentRepositories = (0..10).map {
"/home/user/sample$it"
}
WelcomeView(
recentlyOpenedRepositories = recentRepositories,
newUpdate = null,
onShowCloneDialog = {},
onShowSettings = {},
onOpenRepository = {},
onOpenKnownRepository = {},
onStartRepository = {},
onOpenUrlInBrowser = {},
onRemoveRepositoryFromRecent = {},
)
}
}
@Composable
fun WelcomeView(
recentlyOpenedRepositories: List<String>,
newUpdate: Update?,
onShowCloneDialog: () -> Unit,
onShowSettings: () -> Unit,
onOpenRepository: () -> Unit,
onOpenKnownRepository: (String) -> Unit,
onStartRepository: () -> Unit,
onOpenUrlInBrowser: (String) -> Unit,
onRemoveRepositoryFromRecent: (String) -> Unit,
) {
var showAdditionalInfo by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.surface),
) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = BiasAlignment.Vertical(-0.5f),
Column(
modifier = Modifier.align(Alignment.CenterHorizontally)
.weight(1f),
verticalArrangement = Arrangement.spacedBy(16.dp, BiasAlignment.Vertical(-0.5f)),
) {
HomeButtons(
onOpenRepository = {
val repo = tabViewModel.openDirectoryPicker()
if (repo != null) {
tabViewModel.openRepository(repo)
}
},
onStartRepository = {
val dir = tabViewModel.openDirectoryPicker()
if (dir != null) {
tabViewModel.initLocalRepository(dir)
}
},
onShowCloneView = onShowCloneDialog,
onShowAdditionalInfo = { showAdditionalInfo = true },
onShowSettings = onShowSettings,
onOpenUrlInBrowser = { url -> tabViewModel.openUrlInBrowser(url) }
Text(
text = AppConstants.APP_NAME,
style = MaterialTheme.typography.h1,
maxLines = 1,
modifier = Modifier.padding(bottom = 8.dp),
)
RecentRepositories(appStateManager, tabViewModel)
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),
) {
HomeButtons(
onOpenRepository = onOpenRepository,
onStartRepository = onStartRepository,
onShowCloneView = onShowCloneDialog,
onShowAdditionalInfo = { showAdditionalInfo = true },
onShowSettings = onShowSettings,
onOpenUrlInBrowser = onOpenUrlInBrowser,
)
RecentRepositories(
recentlyOpenedRepositories,
canRepositoriesBeRemoved = true,
onOpenKnownRepository = onOpenKnownRepository,
onRemoveRepositoryFromRecent = onRemoveRepositoryFromRecent,
)
}
}
Spacer(
modifier = Modifier
.height(1.dp)
@ -87,20 +159,25 @@ fun WelcomePage(
.background(MaterialTheme.colors.primaryVariant.copy(alpha = 0.2f))
)
BottomInfoBar(tabViewModel)
BottomInfoBar(
newUpdate = newUpdate,
onOpenUrlInBrowser = onOpenUrlInBrowser,
)
}
if (showAdditionalInfo) {
AppInfoDialog(
onClose = { showAdditionalInfo = false },
onOpenUrlInBrowser = { url -> tabViewModel.openUrlInBrowser(url) }
onOpenUrlInBrowser = onOpenUrlInBrowser
)
}
}
@Composable
private fun BottomInfoBar(tabViewModel: TabViewModel) {
val newUpdate = tabViewModel.hasUpdates.collectAsState().value
private fun BottomInfoBar(
newUpdate: Update?,
onOpenUrlInBrowser: (String) -> Unit,
) {
Row(
modifier = Modifier
@ -115,7 +192,7 @@ private fun BottomInfoBar(tabViewModel: TabViewModel) {
if (newUpdate != null) {
SecondaryButton(
text = "Update ${newUpdate.appVersion} available",
onClick = { tabViewModel.openUrlInBrowser(newUpdate.downloadUrl) },
onClick = { onOpenUrlInBrowser(newUpdate.downloadUrl) },
backgroundButton = MaterialTheme.colors.primary,
modifier = Modifier.padding(end = 16.dp)
)
@ -138,16 +215,7 @@ fun HomeButtons(
onShowSettings: () -> Unit,
onOpenUrlInBrowser: (String) -> Unit,
) {
Column(
modifier = Modifier.padding(end = 32.dp),
) {
Text(
text = AppConstants.APP_NAME,
style = MaterialTheme.typography.h1,
maxLines = 1,
modifier = Modifier.padding(bottom = 16.dp),
)
Column {
ButtonTile(
modifier = Modifier.padding(bottom = 8.dp),
title = "Open a repository",
@ -206,19 +274,23 @@ fun HomeButtons(
}
@Composable
fun RecentRepositories(appStateManager: AppStateManager, tabViewModel: TabViewModel) {
fun RecentRepositories(
recentlyOpenedRepositories: List<String>,
canRepositoriesBeRemoved: Boolean,
onRemoveRepositoryFromRecent: (String) -> Unit,
onOpenKnownRepository: (String) -> Unit,
) {
Column(
modifier = Modifier
.padding(start = 32.dp),
.padding(start = 32.dp)
.width(600.dp)
.height(400.dp),
) {
val latestOpenedRepositoriesPaths = appStateManager.latestOpenedRepositoriesPaths
Text(
text = "Recent",
style = MaterialTheme.typography.h3,
modifier = Modifier.padding(top = 48.dp, bottom = 4.dp),
)
var filter by remember {
mutableStateOf("")
}
if (latestOpenedRepositoriesPaths.isEmpty()) {
if (recentlyOpenedRepositories.isEmpty()) {
Text(
"Nothing to see here, open a repository first!",
color = MaterialTheme.colors.onBackgroundSecondary,
@ -226,47 +298,113 @@ fun RecentRepositories(appStateManager: AppStateManager, tabViewModel: TabViewMo
modifier = Modifier.padding(top = 16.dp)
)
} else {
LazyColumn {
items(items = latestOpenedRepositoriesPaths) { repo ->
val repoDirName = repo.dirName
val repoDirPath = repo.dirPath
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp)
) {
Box(
AdjustableOutlinedTextField(
value = filter,
onValueChange = { filter = it },
hint = "Search for recent repositories",
trailingIcon = {
if (filter.isNotEmpty()) {
IconButton(
onClick = { filter = "" },
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.handMouseClickable {
tabViewModel.openRepository(repo)
},
.size(16.dp)
.handOnHover(),
) {
Text(
text = repoDirName,
style = MaterialTheme.typography.body1,
maxLines = 1,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colors.primaryVariant,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(8.dp)
.widthIn(max = 600.dp),
Icon(
painterResource(AppIcons.CLOSE),
contentDescription = null,
tint = if (filter.isEmpty()) MaterialTheme.colors.onBackgroundSecondary else MaterialTheme.colors.onBackground
)
}
Text(
text = repoDirPath,
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(start = 4.dp)
.widthIn(max = 600.dp),
maxLines = 1,
color = MaterialTheme.colors.onBackgroundSecondary
)
}
}
)
val filteredRepositories = remember(filter, recentlyOpenedRepositories) {
if (filter.isBlank()) {
recentlyOpenedRepositories
} else {
recentlyOpenedRepositories.filter { repository ->
repository.lowercaseContains(filter)
}
}
}
val listState = rememberLazyListState()
Box (modifier = Modifier.padding(top = 4.dp)) {
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
) {
items(items = filteredRepositories) { repo ->
val repoDirName = repo.dirName
val repoDirPath = repo.dirPath
val hoverInteraction = remember { MutableInteractionSource() }
val isHovered by hoverInteraction.collectIsHoveredAsState()
Row(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.fillMaxWidth()
.hoverable(hoverInteraction)
.handMouseClickable { onOpenKnownRepository(repo) }
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
modifier = Modifier.weight(1f),
) {
Text(
text = repoDirName,
style = MaterialTheme.typography.body2,
maxLines = 1,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colors.primaryVariant,
overflow = TextOverflow.Ellipsis,
)
Text(
text = repoDirPath,
style = MaterialTheme.typography.body2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier,
maxLines = 1,
color = MaterialTheme.colors.onBackgroundSecondary
)
}
val buttonAlpha = if (canRepositoriesBeRemoved && isHovered) {
1f
} else {
0f
}
IconButton(
onClick = { onRemoveRepositoryFromRecent(repo) },
enabled = canRepositoriesBeRemoved && isHovered,
modifier = Modifier.alpha(buttonAlpha)
.size(24.dp)
.handOnHover(),
) {
Icon(
painterResource(AppIcons.CLOSE),
contentDescription = "Remove repository from recent",
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.onBackgroundSecondary
)
}
}
}
}
VerticalScrollbar(
rememberScrollbarAdapter(listState),
modifier = Modifier.align(Alignment.CenterEnd)
.fillMaxHeight(),
)
}
}
}

View File

@ -1,47 +0,0 @@
package com.jetpackduba.gitnuro.ui.context_menu
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
@Composable
fun DropDownContent(
dropDownContentData: DropDownContentData,
enabled: Boolean = true,
onDismiss: () -> Unit,
) {
DropdownMenuItem(
enabled = enabled,
onClick = {
dropDownContentData.onClick()
onDismiss()
}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
if (dropDownContentData.icon != null) {
Icon(
painter = painterResource(dropDownContentData.icon),
contentDescription = null,
modifier = Modifier.padding(end = 8.dp),
)
}
Text(
text = dropDownContentData.label,
style = MaterialTheme.typography.body2,
modifier = Modifier.padding(end = 8.dp),
maxLines = 1,
)
}
}
}

View File

@ -1,7 +0,0 @@
package com.jetpackduba.gitnuro.ui.context_menu
data class DropDownContentData(
val label: String,
val icon: String? = null,
val onClick: () -> Unit,
)

View File

@ -1,17 +0,0 @@
package com.jetpackduba.gitnuro.ui.context_menu
import androidx.compose.foundation.ExperimentalFoundationApi
import com.jetpackduba.gitnuro.AppIcons
@OptIn(ExperimentalFoundationApi::class)
fun repositoryAdditionalOptionsMenu(
onOpenRepositoryOnFileExplorer: () -> Unit,
): List<DropDownContentData> {
return mutableListOf(
DropDownContentData(
label = "Open repository folder",
icon = AppIcons.SOURCE,
onClick = onOpenRepositoryOnFileExplorer,
),
)
}

View File

@ -442,7 +442,7 @@ fun SearchFilter(
.padding(end = 4.dp),
onClick = { logViewModel.closeSearch() }
) {
Icon(Icons.Default.Clear, contentDescription = null)
Icon(painterResource(AppIcons.CLOSE), contentDescription = null)
}
}
}

View File

@ -348,7 +348,7 @@ class TabViewModel @Inject constructor(
openRepository(repoDir)
}
val hasUpdates: StateFlow<Update?> = updatesRepository.hasUpdatesFlow()
val update: StateFlow<Update?> = updatesRepository.hasUpdatesFlow()
.flowOn(Dispatchers.IO)
.stateIn(tabScope, started = SharingStarted.Eagerly, null)
@ -449,6 +449,10 @@ class TabViewModel @Inject constructor(
fun openUrlInBrowser(url: String) {
openUrlInBrowserUseCase(url)
}
fun removeRepositoryFromRecent(repository: String) {
appStateManager.removeRepositoryFromRecent(repository)
}
}