diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt b/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt index 607984b..6d398e4 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt @@ -7,8 +7,8 @@ import com.jetpackduba.gitnuro.viewmodels.TextDiffType import com.jetpackduba.gitnuro.viewmodels.textDiffTypeFromValue import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import java.io.File @@ -27,6 +27,7 @@ private const val PREF_WINDOW_PLACEMENT = "windowsPlacement" private const val PREF_CUSTOM_THEME = "customTheme" private const val PREF_UI_SCALE = "ui_scale" private const val PREF_DIFF_TYPE = "diffType" +private const val PREF_SWAP_UNCOMMITED_CHANGES = "inverseUncommitedChanges" private const val PREF_GIT_FF_MERGE = "gitFFMerge" @@ -34,6 +35,7 @@ private const val PREF_GIT_PULL_REBASE = "gitPullRebase" private const val DEFAULT_COMMITS_LIMIT = 1000 private const val DEFAULT_COMMITS_LIMIT_ENABLED = true +private const val DEFAULT_SWAP_UNCOMMITED_CHANGES = false const val DEFAULT_UI_SCALE = -1f @Singleton @@ -41,28 +43,31 @@ class AppSettings @Inject constructor() { private val preferences: Preferences = Preferences.userRoot().node(PREFERENCES_NAME) private val _themeState = MutableStateFlow(theme) - val themeState: StateFlow = _themeState + val themeState = _themeState.asStateFlow() private val _commitsLimitEnabledFlow = MutableStateFlow(commitsLimitEnabled) - val commitsLimitEnabledFlow: MutableStateFlow = _commitsLimitEnabledFlow + val commitsLimitEnabledFlow = _commitsLimitEnabledFlow.asStateFlow() + + private val _swapUncommitedChangesFlow = MutableStateFlow(swapUncommitedChanges) + val swapUncommitedChangesFlow = _swapUncommitedChangesFlow.asStateFlow() private val _ffMergeFlow = MutableStateFlow(ffMerge) - val ffMergeFlow: StateFlow = _ffMergeFlow + val ffMergeFlow = _ffMergeFlow.asStateFlow() private val _pullRebaseFlow = MutableStateFlow(pullRebase) - val pullRebaseFlow: StateFlow = _pullRebaseFlow + val pullRebaseFlow = _pullRebaseFlow.asStateFlow() private val _commitsLimitFlow = MutableSharedFlow() - val commitsLimitFlow: SharedFlow = _commitsLimitFlow + val commitsLimitFlow = _commitsLimitFlow.asSharedFlow() private val _customThemeFlow = MutableStateFlow(null) - val customThemeFlow: StateFlow = _customThemeFlow + val customThemeFlow = _customThemeFlow.asStateFlow() private val _scaleUiFlow = MutableStateFlow(scaleUi) - val scaleUiFlow: StateFlow = _scaleUiFlow + val scaleUiFlow = _scaleUiFlow.asStateFlow() private val _textDiffTypeFlow = MutableStateFlow(textDiffType) - val textDiffTypeFlow: StateFlow = _textDiffTypeFlow + val textDiffTypeFlow = _textDiffTypeFlow.asStateFlow() var latestTabsOpened: String get() = preferences.get(PREF_LATEST_REPOSITORIES_TABS_OPENED, "") @@ -100,6 +105,15 @@ class AppSettings @Inject constructor() { _commitsLimitEnabledFlow.value = value } + var swapUncommitedChanges: Boolean + get() { + return preferences.getBoolean(PREF_SWAP_UNCOMMITED_CHANGES, DEFAULT_SWAP_UNCOMMITED_CHANGES) + } + set(value) { + preferences.putBoolean(PREF_SWAP_UNCOMMITED_CHANGES, value) + _swapUncommitedChangesFlow.value = value + } + var scaleUi: Float get() { return preferences.getFloat(PREF_UI_SCALE, DEFAULT_UI_SCALE) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt index e145272..b7128b5 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt @@ -14,16 +14,12 @@ import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape @@ -44,7 +40,10 @@ import com.jetpackduba.gitnuro.keybindings.KeybindingOption import com.jetpackduba.gitnuro.keybindings.matchesBinding import com.jetpackduba.gitnuro.theme.* import com.jetpackduba.gitnuro.ui.components.* -import com.jetpackduba.gitnuro.ui.context_menu.* +import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu +import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement +import com.jetpackduba.gitnuro.ui.context_menu.EntryType +import com.jetpackduba.gitnuro.ui.context_menu.statusEntriesContextMenuItems import com.jetpackduba.gitnuro.ui.dialogs.CommitAuthorDialog import com.jetpackduba.gitnuro.viewmodels.CommitterDataRequestState import com.jetpackduba.gitnuro.viewmodels.StageState @@ -62,6 +61,7 @@ fun UncommitedChanges( onHistoryFile: (String) -> Unit, ) { val stageStatus = statusViewModel.stageState.collectAsState().value + val swapUncommitedChanges by statusViewModel.swapUncommitedChanges.collectAsState() var commitMessage by remember(statusViewModel) { mutableStateOf(statusViewModel.savedCommitMessage.message) } val stagedListState by statusViewModel.stagedLazyListState.collectAsState() val unstagedListState by statusViewModel.unstagedLazyListState.collectAsState() @@ -129,88 +129,100 @@ fun UncommitedChanges( Column( modifier = Modifier.weight(1f) ) { - EntriesList( - modifier = Modifier - .weight(5f) - .padding(bottom = 4.dp) - .fillMaxWidth(), - title = "Staged", - allActionTitle = "Unstage all", - actionTitle = "Unstage", - actionIcon = AppIcons.REMOVE_DONE, - selectedEntryType = if (selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null, - actionColor = MaterialTheme.colors.error, - actionTextColor = MaterialTheme.colors.onError, - statusEntries = staged, - lazyListState = stagedListState, - onDiffEntrySelected = onStagedDiffEntrySelected, - showSearch = showSearchStaged, - searchFilter = searchFilterStaged, - onSearchFilterToggled = { - statusViewModel.onSearchFilterToggledStaged(it) - }, - onSearchFilterChanged = { - statusViewModel.onSearchFilterChangedStaged(it) - }, - onDiffEntryOptionSelected = { - statusViewModel.unstage(it) - }, - onGenerateContextMenu = { statusEntry -> - statusEntriesContextMenuItems( - statusEntry = statusEntry, - entryType = EntryType.STAGED, - onBlame = { onBlameFile(statusEntry.filePath) }, - onReset = { statusViewModel.resetStaged(statusEntry) }, - onHistory = { onHistoryFile(statusEntry.filePath) }, - ) - }, - onAllAction = { - statusViewModel.unstageAll() - }, - ) + val stagedView: @Composable () -> Unit = { + EntriesList( + modifier = Modifier + .weight(5f) + .padding(bottom = 4.dp) + .fillMaxWidth(), + title = "Staged", + allActionTitle = "Unstage all", + actionTitle = "Unstage", + actionIcon = AppIcons.REMOVE_DONE, + selectedEntryType = if (selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null, + actionColor = MaterialTheme.colors.error, + actionTextColor = MaterialTheme.colors.onError, + statusEntries = staged, + lazyListState = stagedListState, + onDiffEntrySelected = onStagedDiffEntrySelected, + showSearch = showSearchStaged, + searchFilter = searchFilterStaged, + onSearchFilterToggled = { + statusViewModel.onSearchFilterToggledStaged(it) + }, + onSearchFilterChanged = { + statusViewModel.onSearchFilterChangedStaged(it) + }, + onDiffEntryOptionSelected = { + statusViewModel.unstage(it) + }, + onGenerateContextMenu = { statusEntry -> + statusEntriesContextMenuItems( + statusEntry = statusEntry, + entryType = EntryType.STAGED, + onBlame = { onBlameFile(statusEntry.filePath) }, + onReset = { statusViewModel.resetStaged(statusEntry) }, + onHistory = { onHistoryFile(statusEntry.filePath) }, + ) + }, + onAllAction = { + statusViewModel.unstageAll() + }, + ) + } - EntriesList( - modifier = Modifier - .weight(5f) - .padding(bottom = 4.dp) - .fillMaxWidth(), - title = "Unstaged", - actionTitle = "Stage", - actionIcon = AppIcons.DONE, - selectedEntryType = if (selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null, - actionColor = MaterialTheme.colors.primary, - actionTextColor = MaterialTheme.colors.onPrimary, - statusEntries = unstaged, - lazyListState = unstagedListState, - onDiffEntrySelected = onUnstagedDiffEntrySelected, - showSearch = showSearchUnstaged, - searchFilter = searchFilterUnstaged, - onSearchFilterToggled = { - statusViewModel.onSearchFilterToggledUnstaged(it) - }, - onSearchFilterChanged = { - statusViewModel.onSearchFilterChangedUnstaged(it) - }, - onDiffEntryOptionSelected = { - statusViewModel.stage(it) - }, - onGenerateContextMenu = { statusEntry -> - statusEntriesContextMenuItems( - statusEntry = statusEntry, - entryType = EntryType.UNSTAGED, - onBlame = { onBlameFile(statusEntry.filePath) }, - onHistory = { onHistoryFile(statusEntry.filePath) }, - onReset = { statusViewModel.resetUnstaged(statusEntry) }, - onDelete = { - statusViewModel.deleteFile(statusEntry) - }, - ) - }, - onAllAction = { - statusViewModel.stageAll() - }, - allActionTitle = "Stage all", - ) + val unstagedView: @Composable () -> Unit = { + EntriesList( + modifier = Modifier + .weight(5f) + .padding(bottom = 4.dp) + .fillMaxWidth(), + title = "Unstaged", + actionTitle = "Stage", + actionIcon = AppIcons.DONE, + selectedEntryType = if (selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null, + actionColor = MaterialTheme.colors.primary, + actionTextColor = MaterialTheme.colors.onPrimary, + statusEntries = unstaged, + lazyListState = unstagedListState, + onDiffEntrySelected = onUnstagedDiffEntrySelected, + showSearch = showSearchUnstaged, + searchFilter = searchFilterUnstaged, + onSearchFilterToggled = { + statusViewModel.onSearchFilterToggledUnstaged(it) + }, + onSearchFilterChanged = { + statusViewModel.onSearchFilterChangedUnstaged(it) + }, + onDiffEntryOptionSelected = { + statusViewModel.stage(it) + }, + onGenerateContextMenu = { statusEntry -> + statusEntriesContextMenuItems( + statusEntry = statusEntry, + entryType = EntryType.UNSTAGED, + onBlame = { onBlameFile(statusEntry.filePath) }, + onHistory = { onHistoryFile(statusEntry.filePath) }, + onReset = { statusViewModel.resetUnstaged(statusEntry) }, + onDelete = { + statusViewModel.deleteFile(statusEntry) + }, + ) + }, + onAllAction = { + statusViewModel.stageAll() + }, + allActionTitle = "Stage all", + ) + } + + if (swapUncommitedChanges) { + unstagedView() + stagedView() + } else { + stagedView() + unstagedView() + } } Column( 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 b459b46..503ba5e 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 @@ -40,7 +40,7 @@ sealed interface SettingsEntry { val settings = listOf( SettingsEntry.Section("User interface"), SettingsEntry.Entry(AppIcons.PALETTE, "Appearance") { UiSettings(it) }, - SettingsEntry.Entry(AppIcons.LAYOUT, "Layout") { }, + SettingsEntry.Entry(AppIcons.LAYOUT, "Layout") { Layout(it) }, SettingsEntry.Section("GIT"), SettingsEntry.Entry(AppIcons.LIST, "Commits history") { GitSettings(it) }, @@ -234,6 +234,20 @@ fun GitSettings(settingsViewModel: SettingsViewModel) { ) } +@Composable +fun Layout(settingsViewModel: SettingsViewModel) { + val swapUncommitedChanges by settingsViewModel.swapUncommitedChangesFlow.collectAsState() + + SettingToggle( + title = "Swap position for staged/unstaged views", + subtitle = "Show the list of unstaged changes above the list of staged changes", + value = swapUncommitedChanges, + onValueChanged = { value -> + settingsViewModel.swapUncommitedChanges = value + } + ) +} + @Composable fun UiSettings(settingsViewModel: SettingsViewModel) { val currentTheme by settingsViewModel.themeState.collectAsState() diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt index 8495a91..e2095a8 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt @@ -25,6 +25,7 @@ class SettingsViewModel @Inject constructor( val ffMergeFlow = appSettings.ffMergeFlow val pullRebaseFlow = appSettings.pullRebaseFlow val commitsLimitEnabledFlow = appSettings.commitsLimitEnabledFlow + val swapUncommitedChangesFlow = appSettings.swapUncommitedChangesFlow var scaleUi: Float get() = appSettings.scaleUi @@ -38,6 +39,12 @@ class SettingsViewModel @Inject constructor( appSettings.commitsLimitEnabled = value } + var swapUncommitedChanges: Boolean + get() = appSettings.swapUncommitedChanges + set(value) { + appSettings.swapUncommitedChanges = value + } + var ffMerge: Boolean get() = appSettings.ffMerge set(value) { diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StatusViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StatusViewModel.kt index 0ce3858..9664781 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StatusViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StatusViewModel.kt @@ -18,6 +18,7 @@ import com.jetpackduba.gitnuro.git.rebase.SkipRebaseUseCase import com.jetpackduba.gitnuro.git.repository.ResetRepositoryStateUseCase import com.jetpackduba.gitnuro.git.workspace.* import com.jetpackduba.gitnuro.models.AuthorInfo +import com.jetpackduba.gitnuro.preferences.AppSettings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* @@ -52,6 +53,7 @@ class StatusViewModel @Inject constructor( private val loadAuthorUseCase: LoadAuthorUseCase, private val saveAuthorUseCase: SaveAuthorUseCase, private val tabScope: CoroutineScope, + private val appSettings: AppSettings, ) { private val _showSearchUnstaged = MutableStateFlow(false) val showSearchUnstaged: StateFlow = _showSearchUnstaged @@ -65,6 +67,8 @@ class StatusViewModel @Inject constructor( private val _searchFilterStaged = MutableStateFlow(TextFieldValue("")) val searchFilterStaged: StateFlow = _searchFilterStaged + val swapUncommitedChanges = appSettings.swapUncommitedChangesFlow + private val _stageState = MutableStateFlow(StageState.Loading) val stageState: StateFlow = combine(