From ab8e8c7dbe513dbc5b32179c2ff339a4544f0a13 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Wed, 22 Jun 2022 00:53:10 +0200 Subject: [PATCH] Added dialog to edit author info --- .../kotlin/app/extensions/StringExtensions.kt | 5 +- src/main/kotlin/app/git/AuthorManager.kt | 61 +++++++ src/main/kotlin/app/models/AuthorInfo.kt | 8 + .../AuthorInfoSimple.kt} | 4 +- src/main/kotlin/app/ui/RepositoryOpen.kt | 33 +++- .../kotlin/app/ui/dialogs/AuthorDialog.kt | 150 ++++++++++++++++++ .../kotlin/app/viewmodels/AuthorViewModel.kt | 40 +++++ .../kotlin/app/viewmodels/TabViewModel.kt | 37 +++-- 8 files changed, 315 insertions(+), 23 deletions(-) create mode 100644 src/main/kotlin/app/git/AuthorManager.kt create mode 100644 src/main/kotlin/app/models/AuthorInfo.kt rename src/main/kotlin/app/{git/UserInfo.kt => models/AuthorInfoSimple.kt} (51%) create mode 100644 src/main/kotlin/app/ui/dialogs/AuthorDialog.kt create mode 100644 src/main/kotlin/app/viewmodels/AuthorViewModel.kt diff --git a/src/main/kotlin/app/extensions/StringExtensions.kt b/src/main/kotlin/app/extensions/StringExtensions.kt index fee8f83..eb507f9 100644 --- a/src/main/kotlin/app/extensions/StringExtensions.kt +++ b/src/main/kotlin/app/extensions/StringExtensions.kt @@ -39,4 +39,7 @@ val String.lineDelimiter: String? "\n" else null - } \ No newline at end of file + } + +val String.nullIfEmpty: String? + get() = this.ifBlank { null } \ No newline at end of file diff --git a/src/main/kotlin/app/git/AuthorManager.kt b/src/main/kotlin/app/git/AuthorManager.kt new file mode 100644 index 0000000..04ff219 --- /dev/null +++ b/src/main/kotlin/app/git/AuthorManager.kt @@ -0,0 +1,61 @@ +package app.git + +import app.models.AuthorInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.storage.file.FileBasedConfig +import javax.inject.Inject + +class AuthorManager @Inject constructor() { + suspend fun loadAuthor(git: Git) = withContext(Dispatchers.IO) { + val config = git.repository.config + val globalConfig = git.repository.config.baseConfig + + val repoConfig = FileBasedConfig((config as FileBasedConfig).file, git.repository.fs) + repoConfig.load() + + val globalName = globalConfig.getString("user", null, "name") + val globalEmail = globalConfig.getString("user", null, "email") + + val name = repoConfig.getString("user", null, "name") + val email = repoConfig.getString("user", null, "email") + + return@withContext AuthorInfo( + globalName, + globalEmail, + name, + email, + ) + } + + suspend fun saveAuthorInfo(git: Git, newAuthorInfo: AuthorInfo) = withContext(Dispatchers.IO) { + val config = git.repository.config + val globalConfig = git.repository.config.baseConfig + val repoConfig = FileBasedConfig((config as FileBasedConfig).file, git.repository.fs) + repoConfig.load() + + if (globalConfig is FileBasedConfig) { + globalConfig.setStringProperty("user", null, "name", newAuthorInfo.globalName) + globalConfig.setStringProperty("user", null, "email", newAuthorInfo.globalEmail) + globalConfig.save() + } + + config.setStringProperty("user", null, "name", newAuthorInfo.name) + config.setStringProperty("user", null, "email", newAuthorInfo.email) + config.save() + } + + private fun FileBasedConfig.setStringProperty( + section: String, + subsection: String?, + name: String, + value: String?, + ) { + if(value == null) { + unset(section, subsection, name) + } else { + setString(section, subsection, name, value) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/models/AuthorInfo.kt b/src/main/kotlin/app/models/AuthorInfo.kt new file mode 100644 index 0000000..06c83f9 --- /dev/null +++ b/src/main/kotlin/app/models/AuthorInfo.kt @@ -0,0 +1,8 @@ +package app.models + +data class AuthorInfo( + val globalName: String?, + val globalEmail: String?, + val name: String?, + val email: String?, +) \ No newline at end of file diff --git a/src/main/kotlin/app/git/UserInfo.kt b/src/main/kotlin/app/models/AuthorInfoSimple.kt similarity index 51% rename from src/main/kotlin/app/git/UserInfo.kt rename to src/main/kotlin/app/models/AuthorInfoSimple.kt index b68e690..7bf2fde 100644 --- a/src/main/kotlin/app/git/UserInfo.kt +++ b/src/main/kotlin/app/models/AuthorInfoSimple.kt @@ -1,6 +1,6 @@ -package app.git +package app.models -data class UserInfo( +data class AuthorInfoSimple( val name: String?, val email: String?, ) diff --git a/src/main/kotlin/app/ui/RepositoryOpen.kt b/src/main/kotlin/app/ui/RepositoryOpen.kt index ddbc950..012837d 100644 --- a/src/main/kotlin/app/ui/RepositoryOpen.kt +++ b/src/main/kotlin/app/ui/RepositoryOpen.kt @@ -12,10 +12,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import app.extensions.handMouseClickable import app.git.DiffEntryType import app.theme.* +import app.ui.dialogs.AuthorDialog import app.ui.dialogs.NewBranchDialog import app.ui.dialogs.RebaseInteractive import app.ui.dialogs.StashWithMessageDialog @@ -39,7 +42,8 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { val selectedItem by tabViewModel.selectedItem.collectAsState() val blameState by tabViewModel.blameState.collectAsState() val showHistory by tabViewModel.showHistory.collectAsState() - val userInfo by tabViewModel.userInfo.collectAsState() + val userInfo by tabViewModel.authorInfoSimple.collectAsState() + val showAuthorInfo by tabViewModel.showAuthorInfo.collectAsState() var showNewBranchDialog by remember { mutableStateOf(false) } var showStashWithMessageDialog by remember { mutableStateOf(false) } @@ -64,6 +68,16 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { showStashWithMessageDialog = false } ) + } else if (showAuthorInfo) { + val authorViewModel = tabViewModel.authorViewModel + if (authorViewModel != null) { + AuthorDialog( + authorViewModel = authorViewModel, + onClose = { + tabViewModel.closeAuthorInfoDialog() + } + ) + } } Column { @@ -108,11 +122,18 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = "${userInfo.name ?: "Name not set"} <${userInfo.email?: "Email not set"}>", - color = MaterialTheme.colors.primaryTextColor, - fontSize = 12.sp, - ) + Box( + modifier = Modifier + .fillMaxHeight() + .handMouseClickable { tabViewModel.showAuthorInfoDialog() }, + contentAlignment = Alignment.Center, + ) { + Text( + text = "${userInfo.name ?: "Name not set"} <${userInfo.email ?: "Email not set"}>", + color = MaterialTheme.colors.primaryTextColor, + fontSize = 12.sp, + ) + } } } diff --git a/src/main/kotlin/app/ui/dialogs/AuthorDialog.kt b/src/main/kotlin/app/ui/dialogs/AuthorDialog.kt new file mode 100644 index 0000000..1aa1a13 --- /dev/null +++ b/src/main/kotlin/app/ui/dialogs/AuthorDialog.kt @@ -0,0 +1,150 @@ +package app.ui.dialogs + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.mouseClickable +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.isPrimaryPressed +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.theme.outlinedTextFieldColors +import app.theme.primaryTextColor +import app.theme.textButtonColors +import app.ui.components.PrimaryButton +import app.viewmodels.AuthorViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun AuthorDialog( + authorViewModel: AuthorViewModel, + onClose: () -> Unit, +) { + val authorInfo by authorViewModel.authorInfo.collectAsState() + + var globalName by remember(authorInfo) { mutableStateOf(authorInfo.globalName.orEmpty()) } + var globalEmail by remember(authorInfo) { mutableStateOf(authorInfo.globalEmail.orEmpty()) } + var name by remember(authorInfo) { mutableStateOf(authorInfo.name.orEmpty()) } + var email by remember(authorInfo) { mutableStateOf(authorInfo.email.orEmpty()) } + + MaterialDialog(onCloseRequested = onClose) { + Column( + modifier = Modifier + .background(MaterialTheme.colors.background) + .padding(horizontal = 8.dp), + ) { + + Text( + text = "Global settings", + color = MaterialTheme.colors.primaryTextColor, + fontSize = 18.sp, + ) + + TextInput( + title = "Name", + value = globalName, + onValueChange = { globalName = it }, + ) + TextInput( + title = "Email", + value = globalEmail, + onValueChange = { globalEmail = it }, + ) + + Text( + text = "Repository settings", + color = MaterialTheme.colors.primaryTextColor, + fontSize = 18.sp, + modifier = Modifier.padding(top = 16.dp), + ) + + TextInput( + title = "Name", + value = name, + onValueChange = { name = it }, + ) + TextInput( + title = "Email", + value = email, + onValueChange = { email = it }, + ) + + Text( + text = "Repository-level values will override global values", + color = MaterialTheme.colors.primaryTextColor, + fontSize = 14.sp, + modifier = Modifier.padding(top = 8.dp), + ) + Row( + modifier = Modifier + .padding(top = 16.dp) + .align(Alignment.End) + ) { + TextButton( + modifier = Modifier.padding(end = 8.dp), + colors = textButtonColors(), + onClick = { + onClose() + } + ) { + Text("Cancel") + } + PrimaryButton( + onClick = { + authorViewModel.saveAuthorInfo( + globalName, + globalEmail, + name, + email, + ) + onClose() + }, + text = "Save" + ) + } + } + } +} + + +@Composable +private fun TextInput( + title: String, + value: String, + enabled: Boolean = true, + onValueChange: (String) -> Unit, +) { + Row( + modifier = Modifier + .width(400.dp) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + color = MaterialTheme.colors.primaryTextColor, + fontSize = 16.sp, + modifier = Modifier + .width(100.dp) + .padding(end = 16.dp), + ) + + + OutlinedTextField( + value = value, + modifier = Modifier + .weight(1f), + enabled = enabled, + onValueChange = onValueChange, + colors = outlinedTextFieldColors(), + maxLines = 1, + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/AuthorViewModel.kt b/src/main/kotlin/app/viewmodels/AuthorViewModel.kt new file mode 100644 index 0000000..7abe2bb --- /dev/null +++ b/src/main/kotlin/app/viewmodels/AuthorViewModel.kt @@ -0,0 +1,40 @@ +package app.viewmodels + +import app.extensions.nullIfEmpty +import app.git.AuthorManager +import app.git.RefreshType +import app.git.TabState +import app.models.AuthorInfo +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +class AuthorViewModel @Inject constructor( + private val tabState: TabState, + private val authorManager: AuthorManager, +) { + + private val _authorInfo = MutableStateFlow(AuthorInfo(null, null, null, null)) + val authorInfo: StateFlow = _authorInfo + + fun loadAuthorInfo() = tabState.runOperation( + refreshType = RefreshType.NONE, + showError = true, + ) { git -> + _authorInfo.value = authorManager.loadAuthor(git) + } + + fun saveAuthorInfo(globalName: String, globalEmail: String, name: String, email: String) = tabState.runOperation( + showError = true, + refreshType = RefreshType.REPO_STATE, + ) { git -> + val newAuthorInfo = AuthorInfo( + globalName.nullIfEmpty, + globalEmail.nullIfEmpty, + name.nullIfEmpty, + email.nullIfEmpty, + ) + + authorManager.saveAuthorInfo(git, newAuthorInfo) + } +} diff --git a/src/main/kotlin/app/viewmodels/TabViewModel.kt b/src/main/kotlin/app/viewmodels/TabViewModel.kt index 86080f3..2f61322 100644 --- a/src/main/kotlin/app/viewmodels/TabViewModel.kt +++ b/src/main/kotlin/app/viewmodels/TabViewModel.kt @@ -6,6 +6,7 @@ import app.credentials.CredentialsState import app.credentials.CredentialsStateManager import app.git.* import app.logging.printLog +import app.models.AuthorInfoSimple import app.newErrorNow import app.ui.SelectedItem import app.updates.Update @@ -19,7 +20,6 @@ import org.eclipse.jgit.blame.BlameResult import org.eclipse.jgit.lib.Repository import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.revwalk.RevCommit -import org.eclipse.jgit.storage.file.FileBasedConfig import java.io.File import javax.inject.Inject import javax.inject.Provider @@ -46,6 +46,7 @@ class TabViewModel @Inject constructor( private val diffViewModelProvider: Provider, private val rebaseInteractiveViewModelProvider: Provider, private val historyViewModelProvider: Provider, + private val authorViewModelProvider: Provider, private val repositoryManager: RepositoryManager, private val tabState: TabState, val appStateManager: AppStateManager, @@ -88,12 +89,18 @@ class TabViewModel @Inject constructor( private val _showHistory = MutableStateFlow(false) val showHistory: StateFlow = _showHistory - private val _userInfo = MutableStateFlow(UserInfo(null, null)) - val userInfo: StateFlow = _userInfo + private val _showAuthorInfo = MutableStateFlow(false) + val showAuthorInfo: StateFlow = _showAuthorInfo + + private val _authorInfoSimple = MutableStateFlow(AuthorInfoSimple(null, null)) + val authorInfoSimple: StateFlow = _authorInfoSimple var historyViewModel: HistoryViewModel? = null private set + var authorViewModel: AuthorViewModel? = null + private set + val showError = MutableStateFlow(false) init { @@ -185,27 +192,29 @@ class TabViewModel @Inject constructor( printLog(TAG, "Refreshing repository state $newRepoState") _repositoryState.value = newRepoState - loadConfigInfo(git) + loadAuthorInfo(git) onRepositoryStateChanged(newRepoState) } - private fun loadConfigInfo(git: Git) { + private fun loadAuthorInfo(git: Git) { val config = git.repository.config config.load() val userName = config.getString("user", null, "name") val email = config.getString("user", null, "email") - // TODO Load file-specific config -// val fcfg = FileBasedConfig((config as FileBasedConfig).file, git.repository.fs) -// fcfg.load() -// val fname = fcfg.getString("user", null, "name") -// val fmail = fcfg.getString("user", null, "email") -// println("Fname $fname\nFmail $fmail") + _authorInfoSimple.value = AuthorInfoSimple(userName, email) + } - println(userName) - println(email) - _userInfo.value = UserInfo(userName, email) + fun showAuthorInfoDialog() { + authorViewModel = authorViewModelProvider.get() + authorViewModel?.loadAuthorInfo() + _showAuthorInfo.value = true + } + + fun closeAuthorInfoDialog() { + _showAuthorInfo.value = false + authorViewModel = null } private fun onRepositoryStateChanged(newRepoState: RepositoryState) {