Added basic split diff

This commit is contained in:
Abdelilah El Aissaoui 2022-08-13 03:00:55 +02:00
parent 34164fb2bc
commit 5659bf8918
23 changed files with 661 additions and 188 deletions

View File

@ -26,8 +26,10 @@ import androidx.compose.ui.window.rememberWindowState
import app.di.DaggerAppComponent import app.di.DaggerAppComponent
import app.extensions.preferenceValue import app.extensions.preferenceValue
import app.extensions.toWindowPlacement import app.extensions.toWindowPlacement
import app.preferences.AppPreferences import app.logging.printLog
import app.preferences.AppSettings
import app.theme.AppTheme import app.theme.AppTheme
import app.theme.Theme
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.secondaryTextColor import app.theme.secondaryTextColor
import app.ui.AppTab import app.ui.AppTab
@ -40,6 +42,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
private const val TAG = "App"
class App { class App {
private val appComponent = DaggerAppComponent.create() private val appComponent = DaggerAppComponent.create()
@ -48,7 +51,7 @@ class App {
lateinit var appStateManager: AppStateManager lateinit var appStateManager: AppStateManager
@Inject @Inject
lateinit var appPreferences: AppPreferences lateinit var appSettings: AppSettings
@Inject @Inject
lateinit var settingsViewModel: SettingsViewModel lateinit var settingsViewModel: SettingsViewModel
@ -60,17 +63,26 @@ class App {
private val tabsFlow = MutableStateFlow<List<TabInformation>>(emptyList()) private val tabsFlow = MutableStateFlow<List<TabInformation>>(emptyList())
fun start() { fun start() {
val windowPlacement = appPreferences.windowPlacement.toWindowPlacement val windowPlacement = appSettings.windowPlacement.toWindowPlacement
appStateManager.loadRepositoriesTabs() appStateManager.loadRepositoriesTabs()
appPreferences.loadCustomTheme()
try {
if (appSettings.theme == Theme.CUSTOM) {
appSettings.loadCustomTheme()
}
} catch (ex: Exception) {
printLog(TAG, "Failed to load custom theme")
ex.printStackTrace()
}
loadTabs() loadTabs()
application { application {
var isOpen by remember { mutableStateOf(true) } var isOpen by remember { mutableStateOf(true) }
val theme by appPreferences.themeState.collectAsState() val theme by appSettings.themeState.collectAsState()
val customTheme by appPreferences.customThemeFlow.collectAsState() val customTheme by appSettings.customThemeFlow.collectAsState()
val scale by appPreferences.scaleUiFlow.collectAsState() val scale by appSettings.scaleUiFlow.collectAsState()
val windowState = rememberWindowState( val windowState = rememberWindowState(
placement = windowPlacement, placement = windowPlacement,
@ -78,7 +90,7 @@ class App {
) )
// Save window state for next time the Window is started // Save window state for next time the Window is started
appPreferences.windowPlacement = windowState.placement.preferenceValue appSettings.windowPlacement = windowState.placement.preferenceValue
if (isOpen) { if (isOpen) {
Window( Window(
@ -191,6 +203,7 @@ class App {
tabsFlow.value = tabsFlow.value.toMutableList().apply { add(tabInformation) } tabsFlow.value = tabsFlow.value.toMutableList().apply { add(tabInformation) }
} }
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun Tabs( fun Tabs(
selectedTabKey: MutableState<Int>, selectedTabKey: MutableState<Int>,

View File

@ -31,5 +31,8 @@ object AppConstants {
private val apache__2_0 = License("Apache 2.0", "https://www.apache.org/licenses/LICENSE-2.0") private val apache__2_0 = License("Apache 2.0", "https://www.apache.org/licenses/LICENSE-2.0")
private val edl = License("EDL", "https://www.eclipse.org/org/documents/edl-v10.php") private val edl = License("EDL", "https://www.eclipse.org/org/documents/edl-v10.php")
data class License(val name: String, val url: String) data class License(
val name: String,
val url: String
)
data class Project(val name: String, val url: String, val license: License) data class Project(val name: String, val url: String, val license: License)

View File

@ -1,6 +1,6 @@
package app package app
import app.preferences.AppPreferences import app.preferences.AppSettings
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
@ -11,7 +11,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class AppStateManager @Inject constructor( class AppStateManager @Inject constructor(
private val appPreferences: AppPreferences, private val appSettings: AppSettings,
) { ) {
private val mutex = Mutex() private val mutex = Mutex()
@ -59,15 +59,15 @@ class AppStateManager @Inject constructor(
private suspend fun updateSavedRepositoryTabs() = withContext(Dispatchers.IO) { private suspend fun updateSavedRepositoryTabs() = withContext(Dispatchers.IO) {
val tabsList = _openRepositoriesPaths.map { it.value } val tabsList = _openRepositoriesPaths.map { it.value }
appPreferences.latestTabsOpened = Json.encodeToString(tabsList) appSettings.latestTabsOpened = Json.encodeToString(tabsList)
} }
private suspend fun updateLatestRepositoryTabs() = withContext(Dispatchers.IO) { private suspend fun updateLatestRepositoryTabs() = withContext(Dispatchers.IO) {
appPreferences.latestOpenedRepositoriesPath = Json.encodeToString(_latestOpenedRepositoriesPaths) appSettings.latestOpenedRepositoriesPath = Json.encodeToString(_latestOpenedRepositoriesPaths)
} }
fun loadRepositoriesTabs() { fun loadRepositoriesTabs() {
val repositoriesSaved = appPreferences.latestTabsOpened val repositoriesSaved = appSettings.latestTabsOpened
if (repositoriesSaved.isNotEmpty()) { if (repositoriesSaved.isNotEmpty()) {
val repositoriesList = Json.decodeFromString<List<String>>(repositoriesSaved) val repositoriesList = Json.decodeFromString<List<String>>(repositoriesSaved)
@ -77,7 +77,7 @@ class AppStateManager @Inject constructor(
} }
} }
val repositoriesPathsSaved = appPreferences.latestOpenedRepositoriesPath val repositoriesPathsSaved = appSettings.latestOpenedRepositoriesPath
if (repositoriesPathsSaved.isNotEmpty()) { if (repositoriesPathsSaved.isNotEmpty()) {
val repositories = Json.decodeFromString<List<String>>(repositoriesPathsSaved) val repositories = Json.decodeFromString<List<String>>(repositoriesPathsSaved)
_latestOpenedRepositoriesPaths.addAll(repositories) _latestOpenedRepositoriesPaths.addAll(repositories)

View File

@ -2,7 +2,7 @@ package app.di
import app.App import app.App
import app.AppStateManager import app.AppStateManager
import app.preferences.AppPreferences import app.preferences.AppSettings
import dagger.Component import dagger.Component
import javax.inject.Singleton import javax.inject.Singleton
@ -12,5 +12,5 @@ interface AppComponent {
fun inject(main: App) fun inject(main: App)
fun appStateManager(): AppStateManager fun appStateManager(): AppStateManager
fun appPreferences(): AppPreferences fun appPreferences(): AppSettings
} }

View File

@ -118,3 +118,12 @@ val DiffEntry.iconColor: Color
else -> throw NotImplementedError("Unexpected ChangeType") else -> throw NotImplementedError("Unexpected ChangeType")
} }
} }
fun DiffEntry.toStatusType(): StatusType = when (this.changeType) {
DiffEntry.ChangeType.ADD -> StatusType.ADDED
DiffEntry.ChangeType.MODIFY -> StatusType.MODIFIED
DiffEntry.ChangeType.DELETE -> StatusType.REMOVED
DiffEntry.ChangeType.COPY -> StatusType.ADDED
DiffEntry.ChangeType.RENAME -> StatusType.MODIFIED
else -> throw NotImplementedError("Unexpected ChangeType")
}

View File

@ -1,11 +1,25 @@
package app.git package app.git
import app.extensions.filePath
import app.extensions.toStatusType
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
sealed class DiffEntryType { sealed class DiffEntryType {
class CommitDiff(val diffEntry: DiffEntry) : DiffEntryType() class CommitDiff(val diffEntry: DiffEntry) : DiffEntryType() {
override val filePath: String
get() = diffEntry.filePath
sealed class UncommitedDiff(val statusEntry: StatusEntry) : DiffEntryType() override val statusType: StatusType
get() = diffEntry.toStatusType()
}
sealed class UncommitedDiff(val statusEntry: StatusEntry) : DiffEntryType() {
override val filePath: String
get() = statusEntry.filePath
override val statusType: StatusType
get() = statusEntry.statusType
}
sealed class UnstagedDiff(statusEntry: StatusEntry) : UncommitedDiff(statusEntry) sealed class UnstagedDiff(statusEntry: StatusEntry) : UncommitedDiff(statusEntry)
sealed class StagedDiff(statusEntry: StatusEntry) : UncommitedDiff(statusEntry) sealed class StagedDiff(statusEntry: StatusEntry) : UncommitedDiff(statusEntry)
@ -23,4 +37,7 @@ sealed class DiffEntryType {
class SafeStagedDiff(statusEntry: StatusEntry) : StagedDiff(statusEntry) class SafeStagedDiff(statusEntry: StatusEntry) : StagedDiff(statusEntry)
class SafeUnstagedDiff(statusEntry: StatusEntry) : UnstagedDiff(statusEntry) class SafeUnstagedDiff(statusEntry: StatusEntry) : UnstagedDiff(statusEntry)
abstract val filePath: String
abstract val statusType: StatusType
} }

View File

@ -131,3 +131,17 @@ fun prepareTreeParser(repository: Repository, commit: RevCommit): AbstractTreeIt
return treeParser return treeParser
} }
} }
enum class TextDiffType(val value: Int) {
SPLIT(0),
UNIFIED(1);
}
fun textDiffTypeFromValue(diffTypeValue: Int): TextDiffType {
return when (diffTypeValue) {
TextDiffType.SPLIT.value -> TextDiffType.SPLIT
TextDiffType.UNIFIED.value -> TextDiffType.UNIFIED
else -> throw NotImplementedError("Diff type not implemented")
}
}

View File

@ -2,6 +2,8 @@ package app.git.diff
data class Hunk(val header: String, val lines: List<Line>) data class Hunk(val header: String, val lines: List<Line>)
data class SplitHunk(val hunk: Hunk, val lines: List<Pair<Line?, Line?>>)
data class Line(val text: String, val oldLineNumber: Int, val newLineNumber: Int, val lineType: LineType) { data class Line(val text: String, val oldLineNumber: Int, val newLineNumber: Int, val lineType: LineType) {
// lines numbers are stored based on 0 being the first one but on a file the first line is the 1, so increment it! // lines numbers are stored based on 0 being the first one but on a file the first line is the 1, so increment it!
val displayOldLineNumber: Int = oldLineNumber + 1 val displayOldLineNumber: Int = oldLineNumber + 1

View File

@ -221,6 +221,10 @@ sealed class DiffResult(
diffEntry: DiffEntry, diffEntry: DiffEntry,
val hunks: List<Hunk> val hunks: List<Hunk>
) : DiffResult(diffEntry) ) : DiffResult(diffEntry)
class TextSplit(
diffEntry: DiffEntry,
val hunks: List<SplitHunk>
) : DiffResult(diffEntry)
class NonText( class NonText(
diffEntry: DiffEntry, diffEntry: DiffEntry,

View File

@ -1,6 +1,8 @@
package app.preferences package app.preferences
import app.extensions.defaultWindowPlacement import app.extensions.defaultWindowPlacement
import app.git.TextDiffType
import app.git.textDiffTypeFromValue
import app.theme.ColorsScheme import app.theme.ColorsScheme
import app.theme.Theme import app.theme.Theme
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -22,6 +24,7 @@ private const val PREF_COMMITS_LIMIT_ENABLED = "commitsLimitEnabled"
private const val PREF_WINDOW_PLACEMENT = "windowsPlacement" private const val PREF_WINDOW_PLACEMENT = "windowsPlacement"
private const val PREF_CUSTOM_THEME = "customTheme" private const val PREF_CUSTOM_THEME = "customTheme"
private const val PREF_UI_SCALE = "ui_scale" private const val PREF_UI_SCALE = "ui_scale"
private const val PREF_DIFF_TYPE = "diffType"
private const val PREF_GIT_FF_MERGE = "gitFFMerge" private const val PREF_GIT_FF_MERGE = "gitFFMerge"
@ -31,7 +34,7 @@ private const val DEFAULT_COMMITS_LIMIT_ENABLED = true
const val DEFAULT_UI_SCALE = -1f const val DEFAULT_UI_SCALE = -1f
@Singleton @Singleton
class AppPreferences @Inject constructor() { class AppSettings @Inject constructor() {
private val preferences: Preferences = Preferences.userRoot().node(PREFERENCES_NAME) private val preferences: Preferences = Preferences.userRoot().node(PREFERENCES_NAME)
private val _themeState = MutableStateFlow(theme) private val _themeState = MutableStateFlow(theme)
@ -52,6 +55,9 @@ class AppPreferences @Inject constructor() {
private val _scaleUiFlow = MutableStateFlow(scaleUi) private val _scaleUiFlow = MutableStateFlow(scaleUi)
val scaleUiFlow: StateFlow<Float> = _scaleUiFlow val scaleUiFlow: StateFlow<Float> = _scaleUiFlow
private val _textDiffTypeFlow = MutableStateFlow(textDiffType)
val textDiffTypeFlow: StateFlow<TextDiffType> = _textDiffTypeFlow
var latestTabsOpened: String var latestTabsOpened: String
get() = preferences.get(PREF_LATEST_REPOSITORIES_TABS_OPENED, "") get() = preferences.get(PREF_LATEST_REPOSITORIES_TABS_OPENED, "")
set(value) { set(value) {
@ -128,6 +134,18 @@ class AppPreferences @Inject constructor() {
preferences.putInt(PREF_WINDOW_PLACEMENT, placement.value) preferences.putInt(PREF_WINDOW_PLACEMENT, placement.value)
} }
var textDiffType: TextDiffType
get() {
val diffTypeValue = preferences.getInt(PREF_DIFF_TYPE, TextDiffType.UNIFIED.value)
return textDiffTypeFromValue(diffTypeValue)
}
set(placement) {
preferences.putInt(PREF_DIFF_TYPE, placement.value)
_textDiffTypeFlow.value = textDiffType
}
fun saveCustomTheme(filePath: String) { fun saveCustomTheme(filePath: String) {
try { try {
val file = File(filePath) val file = File(filePath)

View File

@ -25,6 +25,8 @@ val lightTheme = ColorsScheme(
dialogOverlay = Color(0xAA000000), dialogOverlay = Color(0xAA000000),
normalScrollbar = Color(0xFFCCCCCC), normalScrollbar = Color(0xFFCCCCCC),
hoverScrollbar = Color(0xFF0070D8), hoverScrollbar = Color(0xFF0070D8),
diffLineAdded = Color(0xFFd7ebd0),
diffLineRemoved = Color(0xFFf0d4d4),
) )
@ -50,7 +52,10 @@ val darkBlueTheme = ColorsScheme(
conflictingFile = Color(0xFFFFB638), conflictingFile = Color(0xFFFFB638),
dialogOverlay = Color(0xAA000000), dialogOverlay = Color(0xAA000000),
normalScrollbar = Color(0xFF888888), normalScrollbar = Color(0xFF888888),
hoverScrollbar = Color(0xFFCCCCCC) hoverScrollbar = Color(0xFFCCCCCC),
diffLineAdded = Color(0xFF566f5a),
diffLineRemoved = Color(0xFF6f585e),
) )
val darkGrayTheme = ColorsScheme( val darkGrayTheme = ColorsScheme(
@ -75,5 +80,7 @@ val darkGrayTheme = ColorsScheme(
conflictingFile = Color(0xFFFFB638), conflictingFile = Color(0xFFFFB638),
dialogOverlay = Color(0xAA000000), dialogOverlay = Color(0xAA000000),
normalScrollbar = Color(0xFF888888), normalScrollbar = Color(0xFF888888),
hoverScrollbar = Color(0xFFCCCCCC) hoverScrollbar = Color(0xFFCCCCCC),
diffLineAdded = Color(0xFF5b7059),
diffLineRemoved = Color(0xFF74595c),
) )

View File

@ -14,6 +14,7 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
// TODO Add line added + removed colors, graph colors and icons color for added/modified/removed files.
@Serializable @Serializable
data class ColorsScheme( data class ColorsScheme(
val primary: Color, val primary: Color,
@ -39,6 +40,8 @@ data class ColorsScheme(
val dialogOverlay: Color, val dialogOverlay: Color,
val normalScrollbar: Color, val normalScrollbar: Color,
val hoverScrollbar: Color, val hoverScrollbar: Color,
val diffLineAdded: Color,
val diffLineRemoved: Color,
) { ) {
fun toComposeColors(): Colors { fun toComposeColors(): Colors {
return Colors( return Colors(

View File

@ -92,6 +92,12 @@ val Colors.secondarySurface: Color
val Colors.dialogOverlay: Color val Colors.dialogOverlay: Color
get() = appTheme.dialogOverlay get() = appTheme.dialogOverlay
val Colors.diffLineAdded: Color
get() = appTheme.diffLineAdded
val Colors.diffLineRemoved: Color
get() = appTheme.diffLineRemoved
enum class Theme(val displayName: String) : DropDownOption { enum class Theme(val displayName: String) : DropDownOption {
LIGHT("Light"), LIGHT("Light"),

View File

@ -162,22 +162,36 @@ fun HistoryContentLoaded(
viewDiffResult != null && viewDiffResult != null &&
viewDiffResult is ViewDiffResult.Loaded viewDiffResult is ViewDiffResult.Loaded
) { ) {
val diffResult = viewDiffResult.diffResult when (val diffResult = viewDiffResult.diffResult) {
if (diffResult is DiffResult.Text) { is DiffResult.Text -> {
TextDiff( HunkUnifiedTextDiff(
diffEntryType = viewDiffResult.diffEntryType, diffEntryType = viewDiffResult.diffEntryType,
scrollState = scrollState, scrollState = scrollState,
diffResult = diffResult, diffResult = diffResult,
onUnstageHunk = { _, _ -> }, onUnstageHunk = { _, _ -> },
onStageHunk = { _, _ -> }, onStageHunk = { _, _ -> },
onResetHunk = { _, _ -> }, onResetHunk = { _, _ -> },
) )
} else { }
Box(
modifier = Modifier is DiffResult.TextSplit -> {
.fillMaxSize() HunkSplitTextDiff(
.background(MaterialTheme.colors.background) diffEntryType = viewDiffResult.diffEntryType,
) scrollState = scrollState,
diffResult = diffResult,
onUnstageHunk = { _, _ -> },
onStageHunk = { _, _ -> },
onResetHunk = { _, _ -> },
)
}
else -> {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
)
}
} }
} else { } else {
Box( Box(

View File

@ -10,10 +10,7 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.IconButton import androidx.compose.material.*
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
@ -34,10 +31,7 @@ import androidx.compose.ui.unit.sp
import app.extensions.lineDelimiter import app.extensions.lineDelimiter
import app.extensions.removeLineDelimiters import app.extensions.removeLineDelimiters
import app.extensions.toStringWithSpaces import app.extensions.toStringWithSpaces
import app.git.DiffEntryType import app.git.*
import app.git.EntryContent
import app.git.StatusEntry
import app.git.StatusType
import app.git.diff.DiffResult import app.git.diff.DiffResult
import app.git.diff.Hunk import app.git.diff.Hunk
import app.git.diff.Line import app.git.diff.Line
@ -61,6 +55,7 @@ fun Diff(
onCloseDiffView: () -> Unit, onCloseDiffView: () -> Unit,
) { ) {
val diffResultState = diffViewModel.diffResult.collectAsState() val diffResultState = diffViewModel.diffResult.collectAsState()
val diffType by diffViewModel.diffTypeFlow.collectAsState()
val viewDiffResult = diffResultState.value ?: return val viewDiffResult = diffResultState.value ?: return
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@ -86,6 +81,7 @@ fun Diff(
ViewDiffResult.DiffNotFound -> { ViewDiffResult.DiffNotFound -> {
onCloseDiffView() onCloseDiffView()
} }
is ViewDiffResult.Loaded -> { is ViewDiffResult.Loaded -> {
val diffEntryType = viewDiffResult.diffEntryType val diffEntryType = viewDiffResult.diffEntryType
val diffEntry = viewDiffResult.diffResult.diffEntry val diffEntry = viewDiffResult.diffResult.diffEntry
@ -95,14 +91,16 @@ fun Diff(
diffEntryType = diffEntryType, diffEntryType = diffEntryType,
diffEntry = diffEntry, diffEntry = diffEntry,
onCloseDiffView = onCloseDiffView, onCloseDiffView = onCloseDiffView,
stageFile = { diffViewModel.stageFile(it) }, diffType = diffType,
unstageFile = { diffViewModel.unstageFile(it) }, onStageFile = { diffViewModel.stageFile(it) },
onUnstageFile = { diffViewModel.unstageFile(it) },
onChangeDiffType = { diffViewModel.changeTextDiffType(it) }
) )
if (diffResult is DiffResult.Text) { val scrollState by diffViewModel.lazyListState.collectAsState()
val scrollState by diffViewModel.lazyListState.collectAsState()
TextDiff( when (diffResult) {
is DiffResult.TextSplit -> HunkSplitTextDiff(
diffEntryType = diffEntryType, diffEntryType = diffEntryType,
scrollState = scrollState, scrollState = scrollState,
diffResult = diffResult, diffResult = diffResult,
@ -116,13 +114,40 @@ fun Diff(
diffViewModel.resetHunk(entry, hunk) diffViewModel.resetHunk(entry, hunk)
} }
) )
} else if (diffResult is DiffResult.NonText) {
NonTextDiff(diffResult) is DiffResult.Text -> HunkUnifiedTextDiff(
diffEntryType = diffEntryType,
scrollState = scrollState,
diffResult = diffResult,
onUnstageHunk = { entry, hunk ->
diffViewModel.unstageHunk(entry, hunk)
},
onStageHunk = { entry, hunk ->
diffViewModel.stageHunk(entry, hunk)
},
onResetHunk = { entry, hunk ->
diffViewModel.resetHunk(entry, hunk)
}
)
is DiffResult.NonText -> {
NonTextDiff(diffResult)
}
} }
} }
ViewDiffResult.Loading, ViewDiffResult.None -> {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant) is ViewDiffResult.Loading -> {
Column {
PathOnlyDiffHeader(filePath = viewDiffResult.filePath, onCloseDiffView = onCloseDiffView)
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colors.primaryVariant
)
}
} }
ViewDiffResult.None -> throw NotImplementedError("None should be a possible state in the diff")
} }
@ -225,8 +250,9 @@ fun BinaryDiff() {
) )
} }
@Composable @Composable
fun TextDiff( fun HunkUnifiedTextDiff(
diffEntryType: DiffEntryType, diffEntryType: DiffEntryType,
scrollState: LazyListState, scrollState: LazyListState,
diffResult: DiffResult.Text, diffResult: DiffResult.Text,
@ -246,7 +272,7 @@ fun TextDiff(
item { item {
DisableSelection { DisableSelection {
HunkHeader( HunkHeader(
hunk = hunk, header = hunk.header,
diffEntryType = diffEntryType, diffEntryType = diffEntryType,
onUnstageHunk = { onUnstageHunk(diffResult.diffEntry, hunk) }, onUnstageHunk = { onUnstageHunk(diffResult.diffEntry, hunk) },
onStageHunk = { onStageHunk(diffResult.diffEntry, hunk) }, onStageHunk = { onStageHunk(diffResult.diffEntry, hunk) },
@ -269,9 +295,74 @@ fun TextDiff(
} }
@Composable
fun HunkSplitTextDiff(
diffEntryType: DiffEntryType,
scrollState: LazyListState,
diffResult: DiffResult.TextSplit,
onUnstageHunk: (DiffEntry, Hunk) -> Unit,
onStageHunk: (DiffEntry, Hunk) -> Unit,
onResetHunk: (DiffEntry, Hunk) -> Unit,
) {
val hunks = diffResult.hunks
SelectionContainer {
ScrollableLazyColumn(
modifier = Modifier
.fillMaxSize(),
state = scrollState
) {
for (splitHunk in hunks) {
item {
DisableSelection {
HunkHeader(
header = splitHunk.hunk.header,
diffEntryType = diffEntryType,
onUnstageHunk = { onUnstageHunk(diffResult.diffEntry, splitHunk.hunk) },
onStageHunk = { onStageHunk(diffResult.diffEntry, splitHunk.hunk) },
onResetHunk = { onResetHunk(diffResult.diffEntry, splitHunk.hunk) },
)
}
}
val oldHighestLineNumber = splitHunk.hunk.lines.maxOf { it.displayOldLineNumber }
val newHighestLineNumber = splitHunk.hunk.lines.maxOf { it.displayNewLineNumber }
val highestLineNumber = max(oldHighestLineNumber, newHighestLineNumber)
val highestLineNumberLength = highestLineNumber.toString().count()
items(splitHunk.lines) { linesPair ->
SplitDiffLine(highestLineNumberLength, linesPair.first, linesPair.second)
}
}
}
}
}
@Composable
fun SplitDiffLine(highestLineNumberLength: Int, first: Line?, second: Line?) {
Row(
modifier = Modifier
.background(MaterialTheme.colors.secondarySurface)
) {
Box(
modifier = Modifier
.weight(1f)
) {
if (first != null)
SplitDiffLine(highestLineNumberLength, first, first.oldLineNumber + 1)
}
Box(modifier = Modifier.weight(1f)) {
if (second != null)
SplitDiffLine(highestLineNumberLength, second, second.newLineNumber + 1)
}
}
}
@Composable @Composable
fun HunkHeader( fun HunkHeader(
hunk: Hunk, header: String,
diffEntryType: DiffEntryType, diffEntryType: DiffEntryType,
onUnstageHunk: () -> Unit, onUnstageHunk: () -> Unit,
onStageHunk: () -> Unit, onStageHunk: () -> Unit,
@ -285,7 +376,7 @@ fun HunkHeader(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = hunk.header, text = header,
color = MaterialTheme.colors.primaryTextColor, color = MaterialTheme.colors.primaryTextColor,
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
) )
@ -332,13 +423,16 @@ fun HunkHeader(
} }
} }
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun DiffHeader( private fun DiffHeader(
diffEntryType: DiffEntryType, diffEntryType: DiffEntryType,
diffEntry: DiffEntry, diffEntry: DiffEntry,
diffType: TextDiffType,
onCloseDiffView: () -> Unit, onCloseDiffView: () -> Unit,
stageFile: (StatusEntry) -> Unit, onStageFile: (StatusEntry) -> Unit,
unstageFile: (StatusEntry) -> Unit, onUnstageFile: (StatusEntry) -> Unit,
onChangeDiffType: (TextDiffType) -> Unit,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -362,28 +456,15 @@ fun DiffHeader(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
if (diffEntryType.statusType != StatusType.ADDED && diffEntryType.statusType != StatusType.REMOVED) {
DiffTypeButtons(diffType = diffType, onChangeDiffType = onChangeDiffType)
}
if (diffEntryType is DiffEntryType.UncommitedDiff) { if (diffEntryType is DiffEntryType.UncommitedDiff) {
val buttonText: String UncommitedDiffFileHeaderButtons(
val color: Color diffEntryType,
onUnstageFile = onUnstageFile,
if (diffEntryType is DiffEntryType.StagedDiff) { onStageFile = onStageFile
buttonText = "Unstage file"
color = MaterialTheme.colors.error
} else {
buttonText = "Stage file"
color = MaterialTheme.colors.primary
}
SecondaryButton(
text = buttonText,
backgroundButton = color,
onClick = {
if (diffEntryType is DiffEntryType.StagedDiff) {
unstageFile(diffEntryType.statusEntry)
} else {
stageFile(diffEntryType.statusEntry)
}
}
) )
} }
@ -401,21 +482,120 @@ fun DiffHeader(
} }
} }
@Composable
fun DiffTypeButtons(diffType: TextDiffType, onChangeDiffType: (TextDiffType) -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text(
"Unified",
color = MaterialTheme.colors.primaryTextColor,
style = MaterialTheme.typography.caption,
)
Switch(
checked = diffType == TextDiffType.SPLIT,
onCheckedChange = { checked ->
val newType = if (checked)
TextDiffType.SPLIT
else
TextDiffType.UNIFIED
onChangeDiffType(newType)
},
colors = SwitchDefaults.colors(
uncheckedThumbColor = MaterialTheme.colors.secondaryVariant,
uncheckedTrackColor = MaterialTheme.colors.secondaryVariant,
uncheckedTrackAlpha = 0.54f
)
)
Text(
"Split",
color = MaterialTheme.colors.primaryTextColor,
// modifier = Modifier.padding(horizontal = 4.dp),
style = MaterialTheme.typography.caption,
)
}
}
@Composable
fun UncommitedDiffFileHeaderButtons(
diffEntryType: DiffEntryType.UncommitedDiff,
onUnstageFile: (StatusEntry) -> Unit,
onStageFile: (StatusEntry) -> Unit
) {
val buttonText: String
val color: Color
if (diffEntryType is DiffEntryType.StagedDiff) {
buttonText = "Unstage file"
color = MaterialTheme.colors.error
} else {
buttonText = "Stage file"
color = MaterialTheme.colors.primary
}
SecondaryButton(
text = buttonText,
backgroundButton = color,
onClick = {
if (diffEntryType is DiffEntryType.StagedDiff) {
onUnstageFile(diffEntryType.statusEntry)
} else {
onStageFile(diffEntryType.statusEntry)
}
}
)
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun PathOnlyDiffHeader(
filePath: String,
onCloseDiffView: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.background(MaterialTheme.colors.headerBackground)
.padding(start = 8.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = filePath,
style = MaterialTheme.typography.body2,
maxLines = 1,
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.weight(1f))
IconButton(
onClick = onCloseDiffView,
modifier = Modifier
.pointerHoverIcon(PointerIconDefaults.Hand)
) {
Image(
painter = painterResource("close.svg"),
contentDescription = "Close diff",
colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryTextColor),
)
}
}
}
@Composable @Composable
fun DiffLine( fun DiffLine(
highestLineNumberLength: Int, highestLineNumberLength: Int,
line: Line, line: Line,
) { ) {
val backgroundColor = when (line.lineType) { val backgroundColor = when (line.lineType) {
LineType.ADDED -> { LineType.ADDED -> MaterialTheme.colors.diffLineAdded
Color(0x77a9d49b) LineType.REMOVED -> MaterialTheme.colors.diffLineRemoved
} LineType.CONTEXT -> MaterialTheme.colors.background
LineType.REMOVED -> {
Color(0x77dea2a2)
}
LineType.CONTEXT -> {
MaterialTheme.colors.background
}
} }
Row( Row(
modifier = Modifier modifier = Modifier
@ -442,33 +622,65 @@ fun DiffLine(
) )
} }
Row { DiffLineText(line.text)
Text( }
text = line.text.replace( // TODO this replace is a workaround until this issue gets fixed https://github.com/JetBrains/compose-jb/issues/615 }
"\t",
" " @Composable
).removeLineDelimiters(), fun SplitDiffLine(
modifier = Modifier highestLineNumberLength: Int,
.padding(start = 8.dp) line: Line,
.fillMaxSize(), lineNumber: Int,
fontFamily = FontFamily.Monospace, ) {
style = MaterialTheme.typography.body2, val backgroundColor = when (line.lineType) {
overflow = TextOverflow.Visible, LineType.ADDED -> MaterialTheme.colors.diffLineAdded
LineType.REMOVED -> MaterialTheme.colors.diffLineRemoved
LineType.CONTEXT -> MaterialTheme.colors.background
}
Row(
modifier = Modifier
.background(backgroundColor)
.height(IntrinsicSize.Min)
) {
DisableSelection {
LineNumber(
text = lineNumber.toStringWithSpaces(highestLineNumberLength),
) )
val lineDelimiter = line.text.lineDelimiter
// Display line delimiter in its own text with a maxLines = 1. This will fix the issue
// where copying a line didn't contain the line ending & also fix the issue where the text line would
// display multiple lines even if there is only a single line with a line delimiter at the end
if (lineDelimiter != null) {
Text(
text = lineDelimiter,
maxLines = 1,
)
}
} }
DiffLineText(line.text)
}
}
@Composable
fun DiffLineText(text: String) {
Row {
Text(
text = text.replace(
"\t",
" "
).removeLineDelimiters(),
modifier = Modifier
.padding(start = 8.dp)
.fillMaxSize(),
fontFamily = FontFamily.Monospace,
style = MaterialTheme.typography.body2,
overflow = TextOverflow.Visible,
)
val lineDelimiter = text.lineDelimiter
// Display line delimiter in its own text with a maxLines = 1. This will fix the issue
// where copying a line didn't contain the line ending & also fix the issue where the text line would
// display multiple lines even if there is only a single line with a line delimiter at the end
if (lineDelimiter != null) {
Text(
text = lineDelimiter,
maxLines = 1,
)
}
} }
} }

View File

@ -26,6 +26,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.PathBuilder
import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
@ -102,6 +103,7 @@ fun Log(
val logStatus = logStatusState.value val logStatus = logStatusState.value
val showLogDialog by logViewModel.logDialog.collectAsState() val showLogDialog by logViewModel.logDialog.collectAsState()
val selectedCommit = if (selectedItem is SelectedItem.CommitBasedItem) { val selectedCommit = if (selectedItem is SelectedItem.CommitBasedItem) {
selectedItem.revCommit selectedItem.revCommit
} else { } else {

View File

@ -1,7 +1,7 @@
package app.viewmodels package app.viewmodels
import app.git.* import app.git.*
import app.preferences.AppPreferences import app.preferences.AppSettings
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
@ -14,7 +14,7 @@ class BranchesViewModel @Inject constructor(
private val mergeManager: MergeManager, private val mergeManager: MergeManager,
private val remoteOperationsManager: RemoteOperationsManager, private val remoteOperationsManager: RemoteOperationsManager,
private val tabState: TabState, private val tabState: TabState,
private val appPreferences: AppPreferences, private val appSettings: AppSettings,
) : ExpandableViewModel(true) { ) : ExpandableViewModel(true) {
private val _branches = MutableStateFlow<List<Ref>>(listOf()) private val _branches = MutableStateFlow<List<Ref>>(listOf())
val branches: StateFlow<List<Ref>> val branches: StateFlow<List<Ref>>
@ -51,7 +51,7 @@ class BranchesViewModel @Inject constructor(
fun mergeBranch(ref: Ref) = tabState.safeProcessing( fun mergeBranch(ref: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
mergeManager.mergeBranch(git, ref, appPreferences.ffMerge) mergeManager.mergeBranch(git, ref, appSettings.ffMerge)
} }
fun deleteBranch(branch: Ref) = tabState.safeProcessing( fun deleteBranch(branch: Ref) = tabState.safeProcessing(

View File

@ -1,13 +1,19 @@
//asdasd
package app.viewmodels package app.viewmodels
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import app.exceptions.MissingDiffEntryException import app.exceptions.MissingDiffEntryException
import app.extensions.delayedStateChange import app.extensions.delayedStateChange
import app.git.* import app.git.*
import app.git.diff.Hunk import app.git.diff.*
import app.preferences.AppSettings
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry
import java.lang.Integer.max
import javax.inject.Inject import javax.inject.Inject
private const val DIFF_MIN_TIME_IN_MS_TO_SHOW_LOAD = 200L private const val DIFF_MIN_TIME_IN_MS_TO_SHOW_LOAD = 200L
@ -16,10 +22,30 @@ class DiffViewModel @Inject constructor(
private val tabState: TabState, private val tabState: TabState,
private val diffManager: DiffManager, private val diffManager: DiffManager,
private val statusManager: StatusManager, private val statusManager: StatusManager,
private val settings: AppSettings,
) { ) {
private val _diffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.Loading) private val _diffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.Loading(""))
val diffResult: StateFlow<ViewDiffResult?> = _diffResult val diffResult: StateFlow<ViewDiffResult?> = _diffResult
val diffTypeFlow = settings.textDiffTypeFlow
private var diffEntryType: DiffEntryType? = null
private var diffTypeFlowChangesCount = 0
private var diffJob: Job? = null
init {
tabState.managerScope.launch {
diffTypeFlow.collect {
val diffEntryType = this@DiffViewModel.diffEntryType
if (diffTypeFlowChangesCount > 0 && diffEntryType != null) { // Ignore the first time the flow triggers, we only care about updates
updateDiff(diffEntryType)
}
diffTypeFlowChangesCount++
}
}
}
val lazyListState = MutableStateFlow( val lazyListState = MutableStateFlow(
LazyListState( LazyListState(
0, 0,
@ -27,46 +53,155 @@ class DiffViewModel @Inject constructor(
) )
) )
// TODO Cancel job if the user closed the diff view while loading fun updateDiff(diffEntryType: DiffEntryType) {
fun updateDiff(diffEntryType: DiffEntryType) = tabState.runOperation( diffJob = tabState.runOperation(refreshType = RefreshType.NONE) { git ->
refreshType = RefreshType.NONE, this.diffEntryType = diffEntryType
) { git ->
var oldDiffEntryType: DiffEntryType? = null
val oldDiffResult = _diffResult.value
if (oldDiffResult is ViewDiffResult.Loaded) { var oldDiffEntryType: DiffEntryType? = null
oldDiffEntryType = oldDiffResult.diffEntryType val oldDiffResult = _diffResult.value
}
if (oldDiffResult is ViewDiffResult.Loaded) {
// If it's a different file or different state (index or workdir), reset the scroll state oldDiffEntryType = oldDiffResult.diffEntryType
if (oldDiffEntryType != null && }
oldDiffEntryType is DiffEntryType.UncommitedDiff && diffEntryType is DiffEntryType.UncommitedDiff &&
oldDiffEntryType.statusEntry.filePath != diffEntryType.statusEntry.filePath // If it's a different file or different state (index or workdir), reset the scroll state
) { if (oldDiffEntryType != null &&
lazyListState.value = LazyListState( oldDiffEntryType is DiffEntryType.UncommitedDiff &&
0, diffEntryType is DiffEntryType.UncommitedDiff &&
0 oldDiffEntryType.statusEntry.filePath != diffEntryType.statusEntry.filePath
) ) {
} lazyListState.value = LazyListState(
0,
try { 0
delayedStateChange( )
delayMs = DIFF_MIN_TIME_IN_MS_TO_SHOW_LOAD, }
onDelayTriggered = { _diffResult.value = ViewDiffResult.Loading }
) { val isFirstLoad = oldDiffResult is ViewDiffResult.Loading && oldDiffResult.filePath.isEmpty()
val diffFormat = diffManager.diffFormat(git, diffEntryType)
_diffResult.value = ViewDiffResult.Loaded(diffEntryType, diffFormat) try {
delayedStateChange(
delayMs = if (isFirstLoad) 0 else DIFF_MIN_TIME_IN_MS_TO_SHOW_LOAD,
onDelayTriggered = { _diffResult.value = ViewDiffResult.Loading(diffEntryType.filePath) }
) {
val diffFormat = diffManager.diffFormat(git, diffEntryType)
val diffEntry = diffFormat.diffEntry
if (
diffTypeFlow.value == TextDiffType.SPLIT &&
diffFormat is DiffResult.Text &&
diffEntry.changeType != DiffEntry.ChangeType.ADD &&
diffEntry.changeType != DiffEntry.ChangeType.DELETE
) {
val splitHunkList = generateSplitDiffFormat(diffFormat)
_diffResult.value = ViewDiffResult.Loaded(
diffEntryType,
DiffResult.TextSplit(diffEntry, splitHunkList)
)
} else {
_diffResult.value = ViewDiffResult.Loaded(diffEntryType, diffFormat)
}
}
} catch (ex: Exception) {
if (ex is MissingDiffEntryException) {
tabState.refreshData(refreshType = RefreshType.UNCOMMITED_CHANGES)
_diffResult.value = ViewDiffResult.DiffNotFound
} else
ex.printStackTrace()
} }
} catch (ex: Exception) {
if (ex is MissingDiffEntryException) {
tabState.refreshData(refreshType = RefreshType.UNCOMMITED_CHANGES)
_diffResult.value = ViewDiffResult.DiffNotFound
} else
ex.printStackTrace()
} }
} }
private fun generateSplitDiffFormat(diffFormat: DiffResult.Text): List<SplitHunk> {
val unifiedHunksList = diffFormat.hunks
val hunksList = mutableListOf<SplitHunk>()
for (hunk in unifiedHunksList) {
val linesNewSideCount =
hunk.lines.count { it.lineType == LineType.ADDED || it.lineType == LineType.CONTEXT }
val linesOldSideCount =
hunk.lines.count { it.lineType == LineType.REMOVED || it.lineType == LineType.CONTEXT }
val addedLines = hunk.lines.filter { it.lineType == LineType.ADDED }
val removedLines = hunk.lines.filter { it.lineType == LineType.REMOVED }
val maxLinesCountOfBothParts = max(linesNewSideCount, linesOldSideCount)
val oldLinesArray = arrayOfNulls<Line?>(maxLinesCountOfBothParts)
val newLinesArray = arrayOfNulls<Line?>(maxLinesCountOfBothParts)
val lines = hunk.lines
val firstLineOldNumber = hunk.lines.first().oldLineNumber
val firstLineNewNumber = hunk.lines.first().newLineNumber
val firstLine = if (maxLinesCountOfBothParts == linesOldSideCount) {
firstLineOldNumber
} else
firstLineNewNumber
val contextLines = lines.filter { it.lineType == LineType.CONTEXT }
for (contextLine in contextLines) {
val lineNumber = if (maxLinesCountOfBothParts == linesOldSideCount) {
contextLine.oldLineNumber
} else
contextLine.newLineNumber
oldLinesArray[lineNumber - firstLine] = contextLine
newLinesArray[lineNumber - firstLine] = contextLine
}
for (removedLine in removedLines) {
val previousLinesToCurrent = lines.takeWhile { it != removedLine }
val previousContextLine = previousLinesToCurrent.lastOrNull { it.lineType == LineType.CONTEXT }
val contextArrayPosition = if (previousContextLine != null)
oldLinesArray.indexOf(previousContextLine)
else
-1
// Get the position the list of null position of the array
val availableIndexes =
newLinesArray.mapIndexed { index, line ->
if (line != null)
null
else
index
}.filterNotNull()
val nextAvailableLinePosition = availableIndexes.first { index -> index > contextArrayPosition }
oldLinesArray[nextAvailableLinePosition] = removedLine
}
for (addedLine in addedLines) {
val previousLinesToCurrent = lines.takeWhile { it != addedLine }
val previousContextLine = previousLinesToCurrent.lastOrNull { it.lineType == LineType.CONTEXT }
val contextArrayPosition = if (previousContextLine != null)
newLinesArray.indexOf(previousContextLine)
else
-1
val availableIndexes =
newLinesArray.mapIndexed { index, line -> if (line != null) null else index }.filterNotNull()
val newLinePosition = availableIndexes.first { index -> index > contextArrayPosition }
newLinesArray[newLinePosition] = addedLine
}
val newHunkLines = mutableListOf<Pair<Line?, Line?>>()
for (i in 0 until maxLinesCountOfBothParts) {
val old = oldLinesArray[i]
val new = newLinesArray[i]
newHunkLines.add(old to new)
}
hunksList.add(SplitHunk(hunk, newHunkLines))
}
return hunksList
}
fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation( fun stageHunk(diffEntry: DiffEntry, hunk: Hunk) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
@ -97,6 +232,12 @@ class DiffViewModel @Inject constructor(
) { git -> ) { git ->
statusManager.unstage(git, statusEntry) statusManager.unstage(git, statusEntry)
} }
fun cancelRunningJobs() {
diffJob?.cancel()
}
fun changeTextDiffType(newDiffType: TextDiffType) {
settings.textDiffType = newDiffType
}
} }

View File

@ -7,6 +7,7 @@ import app.git.DiffEntryType
import app.git.DiffManager import app.git.DiffManager
import app.git.RefreshType import app.git.RefreshType
import app.git.TabState import app.git.TabState
import app.preferences.AppSettings
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
@ -15,6 +16,7 @@ import javax.inject.Inject
class HistoryViewModel @Inject constructor( class HistoryViewModel @Inject constructor(
private val tabState: TabState, private val tabState: TabState,
private val diffManager: DiffManager, private val diffManager: DiffManager,
private val settings: AppSettings,
) { ) {
private val _historyState = MutableStateFlow<HistoryState>(HistoryState.Loading("")) private val _historyState = MutableStateFlow<HistoryState>(HistoryState.Loading(""))
val historyState: StateFlow<HistoryState> = _historyState val historyState: StateFlow<HistoryState> = _historyState

View File

@ -6,7 +6,7 @@ import app.extensions.delayedStateChange
import app.git.* import app.git.*
import app.git.graph.GraphCommitList import app.git.graph.GraphCommitList
import app.git.graph.GraphNode import app.git.graph.GraphNode
import app.preferences.AppPreferences import app.preferences.AppSettings
import app.ui.SelectedItem import app.ui.SelectedItem
import app.ui.log.LogDialog import app.ui.log.LogDialog
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -40,7 +40,7 @@ class LogViewModel @Inject constructor(
private val mergeManager: MergeManager, private val mergeManager: MergeManager,
private val remoteOperationsManager: RemoteOperationsManager, private val remoteOperationsManager: RemoteOperationsManager,
private val tabState: TabState, private val tabState: TabState,
private val appPreferences: AppPreferences, private val appSettings: AppSettings,
) { ) {
private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loading) private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loading)
@ -66,12 +66,12 @@ class LogViewModel @Inject constructor(
init { init {
scope.launch { scope.launch {
appPreferences.commitsLimitEnabledFlow.collect { appSettings.commitsLimitEnabledFlow.collect {
tabState.refreshData(RefreshType.ONLY_LOG) tabState.refreshData(RefreshType.ONLY_LOG)
} }
} }
scope.launch { scope.launch {
appPreferences.commitsLimitFlow.collect { appSettings.commitsLimitFlow.collect {
tabState.refreshData(RefreshType.ONLY_LOG) tabState.refreshData(RefreshType.ONLY_LOG)
} }
} }
@ -90,13 +90,13 @@ class LogViewModel @Inject constructor(
) )
val hasUncommitedChanges = statusSummary.total > 0 val hasUncommitedChanges = statusSummary.total > 0
val commitsLimit = if (appPreferences.commitsLimitEnabled) { val commitsLimit = if (appSettings.commitsLimitEnabled) {
appPreferences.commitsLimit appSettings.commitsLimit
} else } else
Int.MAX_VALUE Int.MAX_VALUE
val commitsLimitDisplayed = if (appPreferences.commitsLimitEnabled) { val commitsLimitDisplayed = if (appSettings.commitsLimitEnabled) {
appPreferences.commitsLimit appSettings.commitsLimit
} else } else
-1 -1
@ -176,7 +176,7 @@ class LogViewModel @Inject constructor(
fun mergeBranch(ref: Ref) = tabState.safeProcessing( fun mergeBranch(ref: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
mergeManager.mergeBranch(git, ref, appPreferences.ffMerge) mergeManager.mergeBranch(git, ref, appSettings.ffMerge)
} }
fun deleteBranch(branch: Ref) = tabState.safeProcessing( fun deleteBranch(branch: Ref) = tabState.safeProcessing(

View File

@ -1,60 +1,60 @@
package app.viewmodels package app.viewmodels
import app.preferences.AppPreferences import app.preferences.AppSettings
import app.theme.Theme import app.theme.Theme
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class SettingsViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
val appPreferences: AppPreferences, val appSettings: AppSettings,
) { ) {
// Temporary values to detect changed variables // Temporary values to detect changed variables
var commitsLimit: Int = -1 var commitsLimit: Int = -1
val themeState = appPreferences.themeState val themeState = appSettings.themeState
val customThemeFlow = appPreferences.customThemeFlow val customThemeFlow = appSettings.customThemeFlow
val ffMergeFlow = appPreferences.ffMergeFlow val ffMergeFlow = appSettings.ffMergeFlow
val commitsLimitEnabledFlow = appPreferences.commitsLimitEnabledFlow val commitsLimitEnabledFlow = appSettings.commitsLimitEnabledFlow
var scaleUi: Float var scaleUi: Float
get() = appPreferences.scaleUi get() = appSettings.scaleUi
set(value) { set(value) {
appPreferences.scaleUi = value appSettings.scaleUi = value
} }
var commitsLimitEnabled: Boolean var commitsLimitEnabled: Boolean
get() = appPreferences.commitsLimitEnabled get() = appSettings.commitsLimitEnabled
set(value) { set(value) {
appPreferences.commitsLimitEnabled = value appSettings.commitsLimitEnabled = value
} }
var ffMerge: Boolean var ffMerge: Boolean
get() = appPreferences.ffMerge get() = appSettings.ffMerge
set(value) { set(value) {
appPreferences.ffMerge = value appSettings.ffMerge = value
} }
var theme: Theme var theme: Theme
get() = appPreferences.theme get() = appSettings.theme
set(value) { set(value) {
appPreferences.theme = value appSettings.theme = value
} }
fun saveCustomTheme(filePath: String) { fun saveCustomTheme(filePath: String) {
appPreferences.saveCustomTheme(filePath) appSettings.saveCustomTheme(filePath)
} }
fun resetInfo() { fun resetInfo() {
commitsLimit = appPreferences.commitsLimit commitsLimit = appSettings.commitsLimit
} }
fun savePendingChanges() { fun savePendingChanges() {
val commitsLimit = this.commitsLimit val commitsLimit = this.commitsLimit
if (appPreferences.commitsLimit != commitsLimit) { if (appSettings.commitsLimit != commitsLimit) {
appPreferences.commitsLimit = commitsLimit appSettings.commitsLimit = commitsLimit
} }
} }
} }

View File

@ -352,8 +352,10 @@ class TabViewModel @Inject constructor(
diffViewModel = diffViewModelProvider.get() diffViewModel = diffViewModelProvider.get()
} }
diffViewModel?.cancelRunningJobs()
diffViewModel?.updateDiff(diffSelected) diffViewModel?.updateDiff(diffSelected)
} else { } else {
diffViewModel?.cancelRunningJobs()
diffViewModel = null // Free the view model from the memory if not being used. diffViewModel = null // Free the view model from the memory if not being used.
} }
} }

View File

@ -1,11 +1,15 @@
package app.viewmodels package app.viewmodels
import app.git.DiffEntryType import app.git.DiffEntryType
import app.git.TextDiffType
import app.git.diff.DiffResult import app.git.diff.DiffResult
sealed interface ViewDiffResult { sealed interface ViewDiffResult {
object None : ViewDiffResult object None : ViewDiffResult
object Loading : ViewDiffResult
data class Loading(val filePath: String) : ViewDiffResult
object DiffNotFound : ViewDiffResult object DiffNotFound : ViewDiffResult
data class Loaded(val diffEntryType: DiffEntryType, val diffResult: DiffResult) : ViewDiffResult
data class Loaded(val diffEntryType: DiffEntryType, val diffResult: DiffResult/*, val diffType: TextDiffType*/) : ViewDiffResult
} }