diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/models/CustomAction.kt b/src/main/kotlin/com/jetpackduba/gitnuro/models/CustomAction.kt new file mode 100644 index 00000000..86ade692 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/models/CustomAction.kt @@ -0,0 +1,11 @@ +package com.jetpackduba.gitnuro.models + +import kotlinx.serialization.Serializable + +@Serializable +data class CustomAction( + val id: String, + val name: String, + val command: String, + val icon: String = "bolt" // default icon name +) \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/repositories/AppSettingsRepository.kt b/src/main/kotlin/com/jetpackduba/gitnuro/repositories/AppSettingsRepository.kt index c4b95245..ec4887ca 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/repositories/AppSettingsRepository.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/repositories/AppSettingsRepository.kt @@ -14,9 +14,11 @@ import com.jetpackduba.gitnuro.ui.dialogs.settings.ProxyType import com.jetpackduba.gitnuro.viewmodels.TextDiffType import com.jetpackduba.gitnuro.viewmodels.textDiffTypeFromValue import kotlinx.coroutines.flow.MutableStateFlow +import com.jetpackduba.gitnuro.models.CustomAction import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.serialization.json.Json +import kotlinx.serialization.encodeToString import java.io.File import java.util.prefs.Preferences import javax.inject.Inject @@ -59,6 +61,7 @@ private const val PREF_GIT_PULL_REBASE = "gitPullRebase" private const val PREF_GIT_PUSH_WITH_LEASE = "gitPushWithLease" private const val PREF_VERIFY_SSL = "verifySsl" +private const val PREF_CUSTOM_ACTIONS = "customQuickActions" private const val DEFAULT_SWAP_UNCOMMITTED_CHANGES = false private const val DEFAULT_SHOW_CHANGES_AS_TREE = false @@ -419,6 +422,24 @@ class AppSettingsRepository @Inject constructor() { _proxyFlow.value = _proxyFlow.value.copy(hostPassword = value) } + var customActions: List + get() { + val json = preferences.get(PREF_CUSTOM_ACTIONS, null) + return if (json != null) { + try { + Json.decodeFromString>(json) + } catch (e: Exception) { + emptyList() + } + } else { + emptyList() + } + } + set(value) { + val json = Json.encodeToString(value) + preferences.put(PREF_CUSTOM_ACTIONS, json) + } + fun saveCustomTheme(filePath: String) { val file = File(filePath) val content = file.readText() diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt index 3a65563c..2e933573 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt @@ -92,13 +92,17 @@ fun RepositoryOpenPage( onClose = { showQuickActionsDialog = false }, onAction = { showQuickActionsDialog = false - when (it) { + when (it.type) { QuickActionType.OPEN_DIR_IN_FILE_MANAGER -> repositoryOpenViewModel.openFolderInFileExplorer() QuickActionType.CLONE -> onShowCloneDialog() QuickActionType.REFRESH -> repositoryOpenViewModel.refreshAll() QuickActionType.SIGN_OFF -> showSignOffDialog = true + QuickActionType.CUSTOM_ACTION -> { + it.command?.let { command -> repositoryOpenViewModel.executeCustomAction(command) } + } } }, + customActions = repositoryOpenViewModel.customActions ) } else if (showSignOffDialog) { SignOffDialog( diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/QuickActionsDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/QuickActionsDialog.kt index 0a743fbe..9e76c97e 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/QuickActionsDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/QuickActionsDialog.kt @@ -28,17 +28,22 @@ import org.jetbrains.compose.resources.painterResource @Composable fun QuickActionsDialog( onClose: () -> Unit, - onAction: (QuickActionType) -> Unit, + onAction: (QuickAction) -> Unit, + customActions: List ) { val textFieldFocusRequester = remember { FocusRequester() } - val items = remember { - listOf( - QuickAction(Res.drawable.code, "Open repository in file manager", QuickActionType.OPEN_DIR_IN_FILE_MANAGER), - QuickAction(Res.drawable.download, "Clone new repository", QuickActionType.CLONE), - QuickAction(Res.drawable.refresh, "Refresh repository data", QuickActionType.REFRESH), - QuickAction(Res.drawable.sign, "Signoff config", QuickActionType.SIGN_OFF), - ) + val items = remember(customActions) { + ( + listOf( + QuickAction(Res.drawable.code, "Open repository in file manager", QuickActionType.OPEN_DIR_IN_FILE_MANAGER), + QuickAction(Res.drawable.download, "Clone new repository", QuickActionType.CLONE), + QuickAction(Res.drawable.refresh, "Refresh repository data", QuickActionType.REFRESH), + QuickAction(Res.drawable.sign, "Signoff config", QuickActionType.SIGN_OFF), + ) + customActions.map { + QuickAction(Res.drawable.bolt, it.name, QuickActionType.CUSTOM_ACTION, it.command) + } + ).sortedBy { it.title } } var searchFilter by remember { mutableStateOf("") } @@ -70,7 +75,7 @@ fun QuickActionsDialog( } else if (keyEvent.matchesBinding(KeybindingOption.SIMPLE_ACCEPT)) { val item = filteredItems.getOrNull(selectedIndex) if (item != null) - onAction(item.type) + onAction(item) true } else false @@ -98,7 +103,7 @@ fun QuickActionsDialog( .fillMaxWidth() .clip(RoundedCornerShape(4.dp)) .backgroundIf(selectedIndex == index, MaterialTheme.colors.backgroundSelected) - .handMouseClickable { onAction(item.type) } + .handMouseClickable { onAction(item) } ) { Icon( painterResource(item.icon), @@ -123,11 +128,12 @@ fun QuickActionsDialog( } } -data class QuickAction(val icon: DrawableResource, val title: String, val type: QuickActionType) +data class QuickAction(val icon: DrawableResource, val title: String, val type: QuickActionType, val command: String = "") enum class QuickActionType { OPEN_DIR_IN_FILE_MANAGER, CLONE, REFRESH, - SIGN_OFF + SIGN_OFF, + CUSTOM_ACTION } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt index f7b35162..0c395ace 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt @@ -5,6 +5,8 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.Icon import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme @@ -25,6 +27,7 @@ import com.jetpackduba.gitnuro.extensions.handOnHover import com.jetpackduba.gitnuro.extensions.toSmartSystemString import com.jetpackduba.gitnuro.generated.resources.* import com.jetpackduba.gitnuro.managers.Error +import com.jetpackduba.gitnuro.models.CustomAction import com.jetpackduba.gitnuro.preferences.AvatarProviderType import com.jetpackduba.gitnuro.repositories.DEFAULT_UI_SCALE import com.jetpackduba.gitnuro.theme.* @@ -72,6 +75,8 @@ val settings = listOf( SettingsEntry.Section("Tools"), SettingsEntry.Entry(Res.drawable.terminal, "Terminal") { Terminal(it) }, SettingsEntry.Entry(Res.drawable.info, "Logs") { Logs(it) }, + SettingsEntry.Entry(Res.drawable.bolt, "Custom Actions") { CustomActions(it) }, + ) val linesHeightTypesList = listOf( @@ -370,6 +375,96 @@ fun Terminal(settingsViewModel: SettingsViewModel) { ) } +@Composable +fun CustomActions(settingsViewModel: SettingsViewModel) { + + var actions by remember { mutableStateOf(settingsViewModel.customActions.toMutableList()) } + + Text( + text = "Custom Actions", + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.onBackground, + modifier = Modifier.fillMaxWidth(), + maxLines = 1 + ) + + // Header row + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + ) { + Text( + text = "Name", + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.onBackgroundSecondary, + modifier = Modifier.width(120.dp) + ) + Spacer(Modifier.width(8.dp)) + Text( + text = "Command", + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.onBackgroundSecondary, + modifier = Modifier.weight(1f) + ) + Spacer(Modifier.width(8.dp)) + // Empty space for Remove button + Spacer(Modifier.width(80.dp)) + } + + LazyColumn { + items(actions.size) { index -> + val command = actions[index] + Row(verticalAlignment = Alignment.CenterVertically) { + AdjustableOutlinedTextField( + value = command.name, + modifier = Modifier.width(120.dp), + onValueChange = { newName: String -> + actions = actions.toMutableList().also { it[index] = command.copy(name = newName) } + settingsViewModel.customActions = actions.toList() + }, + singleLine = true + ) + Spacer(Modifier.width(8.dp)) + AdjustableOutlinedTextField( + value = command.command, + modifier = Modifier.weight(1f), + onValueChange = { newCmd: String -> + actions = actions.toMutableList().also { it[index] = command.copy(command = newCmd) } + settingsViewModel.customActions = actions.toList() + }, + singleLine = true, + ) + Spacer(Modifier.width(8.dp)) + PrimaryButton( + text = "Remove", + onClick = { + actions = actions.toMutableList().also { it.removeAt(index) } + settingsViewModel.customActions = actions.toList() + } + ) + } + Spacer(Modifier.height(8.dp)) + } + } + + Spacer(Modifier.height(8.dp)) + PrimaryButton( + text = "Add Action", + onClick = { + actions = actions.toMutableList().apply { + add(CustomAction( + id = java.util.UUID.randomUUID().toString(), + name = "New Action", + command = "", + )) + } + settingsViewModel.customActions = actions.toList() + } + ) +} + @Composable fun Logs(settingsViewModel: SettingsViewModel) { SettingButton( diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RepositoryOpenViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RepositoryOpenViewModel.kt index e6df07fd..dc20261e 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RepositoryOpenViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RepositoryOpenViewModel.kt @@ -13,6 +13,7 @@ import com.jetpackduba.gitnuro.logging.printLog import com.jetpackduba.gitnuro.managers.AppStateManager import com.jetpackduba.gitnuro.managers.ErrorsManager import com.jetpackduba.gitnuro.managers.newErrorNow +import com.jetpackduba.gitnuro.managers.IShellManager import com.jetpackduba.gitnuro.models.AuthorInfoSimple import com.jetpackduba.gitnuro.models.errorNotification import com.jetpackduba.gitnuro.models.positiveNotification @@ -25,6 +26,8 @@ import com.jetpackduba.gitnuro.ui.TabsManager import com.jetpackduba.gitnuro.ui.VerticalSplitPaneConfig import com.jetpackduba.gitnuro.updates.Update import com.jetpackduba.gitnuro.updates.UpdatesRepository +import com.jetpackduba.gitnuro.repositories.AppSettingsRepository + import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -72,6 +75,9 @@ class RepositoryOpenViewModel @Inject constructor( private val globalMenuActionsViewModel: GlobalMenuActionsViewModel, sharedRepositoryStateManager: SharedRepositoryStateManager, updatesRepository: UpdatesRepository, + private val appSettingsRepository: AppSettingsRepository, + private val shellManager: IShellManager, + ) : IVerticalSplitPaneConfig by verticalSplitPaneConfig, IGlobalMenuActionsViewModel by globalMenuActionsViewModel { private val errorsManager: ErrorsManager = tabState.errorsManager @@ -110,6 +116,12 @@ class RepositoryOpenViewModel @Inject constructor( var authorViewModel: AuthorViewModel? = null private set + var customActions: List + get() = appSettingsRepository.customActions + set(value) { + appSettingsRepository.customActions = value + } + private var hasGitDirChanged = false init { @@ -361,8 +373,14 @@ class RepositoryOpenViewModel @Inject constructor( fun closeLastView() = tabScope.launch { tabState.closeLastView() } -} + fun executeCustomAction(command: String) = tabState.runOperation( + showError = true, + refreshType = RefreshType.REPO_STATE, + ) { git -> + shellManager.runCommandInPath(listOf(command), git.repository.workTree.absolutePath) + } +} sealed interface BlameState { data class Loading(val filePath: String) : BlameState diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt index d876aa96..e59afa27 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt @@ -167,6 +167,12 @@ class SettingsViewModel @Inject constructor( appSettingsRepository.proxyHostPassword = value } + var customActions: List + get() = appSettingsRepository.customActions + set(value) { + appSettingsRepository.customActions = value + } + fun saveCustomTheme(filePath: String): Error? { return try { appSettingsRepository.saveCustomTheme(filePath)