Added option to swap staged/unstaged changes in the UI

Fixes #10
This commit is contained in:
Abdelilah El Aissaoui 2023-05-20 19:49:57 +02:00
parent 45e4f9e799
commit 37a65ffc11
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
5 changed files with 148 additions and 97 deletions

View File

@ -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<Theme> = _themeState
val themeState = _themeState.asStateFlow()
private val _commitsLimitEnabledFlow = MutableStateFlow(commitsLimitEnabled)
val commitsLimitEnabledFlow: MutableStateFlow<Boolean> = _commitsLimitEnabledFlow
val commitsLimitEnabledFlow = _commitsLimitEnabledFlow.asStateFlow()
private val _swapUncommitedChangesFlow = MutableStateFlow(swapUncommitedChanges)
val swapUncommitedChangesFlow = _swapUncommitedChangesFlow.asStateFlow()
private val _ffMergeFlow = MutableStateFlow(ffMerge)
val ffMergeFlow: StateFlow<Boolean> = _ffMergeFlow
val ffMergeFlow = _ffMergeFlow.asStateFlow()
private val _pullRebaseFlow = MutableStateFlow(pullRebase)
val pullRebaseFlow: StateFlow<Boolean> = _pullRebaseFlow
val pullRebaseFlow = _pullRebaseFlow.asStateFlow()
private val _commitsLimitFlow = MutableSharedFlow<Int>()
val commitsLimitFlow: SharedFlow<Int> = _commitsLimitFlow
val commitsLimitFlow = _commitsLimitFlow.asSharedFlow()
private val _customThemeFlow = MutableStateFlow<ColorsScheme?>(null)
val customThemeFlow: StateFlow<ColorsScheme?> = _customThemeFlow
val customThemeFlow = _customThemeFlow.asStateFlow()
private val _scaleUiFlow = MutableStateFlow(scaleUi)
val scaleUiFlow: StateFlow<Float> = _scaleUiFlow
val scaleUiFlow = _scaleUiFlow.asStateFlow()
private val _textDiffTypeFlow = MutableStateFlow(textDiffType)
val textDiffTypeFlow: StateFlow<TextDiffType> = _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)

View File

@ -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(

View File

@ -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()

View File

@ -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) {

View File

@ -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<Boolean> = _showSearchUnstaged
@ -65,6 +67,8 @@ class StatusViewModel @Inject constructor(
private val _searchFilterStaged = MutableStateFlow(TextFieldValue(""))
val searchFilterStaged: StateFlow<TextFieldValue> = _searchFilterStaged
val swapUncommitedChanges = appSettings.swapUncommitedChangesFlow
private val _stageState = MutableStateFlow<StageState>(StageState.Loading)
val stageState: StateFlow<StageState> = combine(