Gitnuro/src/main/kotlin/app/ui/Diff.kt
Abdelilah El Aissaoui 6ca1e15ce8 UI improvements
2022-01-31 15:15:05 +01:00

364 lines
11 KiB
Kotlin

package app.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.loadImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.git.DiffEntryType
import app.git.EntryContent
import app.git.diff.DiffResult
import app.git.diff.Hunk
import app.git.diff.Line
import app.git.diff.LineType
import app.theme.primaryTextColor
import app.theme.stageButton
import app.theme.unstageButton
import app.ui.components.ScrollableLazyColumn
import app.ui.components.SecondaryButton
import app.viewmodels.DiffViewModel
import org.eclipse.jgit.diff.DiffEntry
import java.io.FileInputStream
import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.math.max
@Composable
fun Diff(
diffViewModel: DiffViewModel,
onCloseDiffView: () -> Unit,
) {
val diffResultState = diffViewModel.diffResult.collectAsState()
val viewDiffResult = diffResultState.value ?: return
val diffEntryType = viewDiffResult.diffEntryType
val diffEntry = diffEntryType.diffEntry
val diffResult = viewDiffResult.diffResult
Column(
modifier = Modifier
.padding(8.dp)
.background(MaterialTheme.colors.background)
.fillMaxSize()
) {
DiffHeader(diffEntry, onCloseDiffView)
if (diffResult is DiffResult.Text) {
TextDiff(diffEntryType, diffViewModel, diffResult)
} else if (diffResult is DiffResult.NonText) {
NonTextDiff(diffResult)
}
}
}
@Composable
fun NonTextDiff(diffResult: DiffResult.NonText) {
val oldBinaryContent = diffResult.oldBinaryContent
val newBinaryContent = diffResult.newBinaryContent
val showOldAndNew = oldBinaryContent != EntryContent.Missing && newBinaryContent != EntryContent.Missing
Row(
modifier = Modifier
.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
) {
if (showOldAndNew) {
Column(
modifier = Modifier.weight(0.5f)
.padding(start = 24.dp, end = 8.dp, top = 24.dp, bottom = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SideTitle("Old")
SideDiff(oldBinaryContent)
}
Column(
modifier = Modifier.weight(0.5f)
.padding(start = 8.dp, end = 24.dp, top = 24.dp, bottom = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SideTitle("New")
SideDiff(newBinaryContent)
}
} else if(oldBinaryContent != EntryContent.Missing) {
Box(
modifier = Modifier.fillMaxSize()
.padding(all = 24.dp),
) {
SideDiff(oldBinaryContent)
}
} else if(newBinaryContent != EntryContent.Missing) {
Column(
modifier = Modifier.fillMaxSize()
.padding(all = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
SideTitle("Binary file")
Spacer(modifier = Modifier.height(24.dp))
SideDiff(newBinaryContent)
}
}
}
}
@Composable
fun SideTitle(text: String) {
Text(
text = text,
fontSize = 20.sp,
color = MaterialTheme.colors.primaryTextColor,
)
}
@Composable
fun SideDiff(entryContent: EntryContent) {
when (entryContent) {
EntryContent.Binary -> BinaryDiff()
is EntryContent.ImageBinary -> ImageDiff(entryContent.tempFilePath)
else -> {}
// is EntryContent.Text -> //TODO maybe have a text view if the file was a binary before?
// TODO Show some info about this EntryContent.TooLargeEntry -> TODO()
}
}
@Composable
fun ImageDiff(tempImagePath: Path) {
Image(
bitmap = loadImageBitmap(inputStream = FileInputStream(tempImagePath.absolutePathString())),
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
@Composable
fun BinaryDiff() {
Image(
painter = painterResource("binary.svg"),
contentDescription = null,
modifier = Modifier.width(400.dp),
colorFilter = ColorFilter.tint(MaterialTheme.colors.primary)
)
}
@Composable
fun TextDiff(diffEntryType: DiffEntryType, diffViewModel: DiffViewModel, diffResult: DiffResult.Text) {
val hunks = diffResult.hunks
val scrollState by diffViewModel.lazyListState.collectAsState()
ScrollableLazyColumn(
modifier = Modifier
.fillMaxSize(),
state = scrollState
) {
items(hunks) { hunk ->
HunkHeader(
hunk = hunk,
diffEntryType = diffEntryType,
diffViewModel = diffViewModel,
)
SelectionContainer {
Column {
val oldHighestLineNumber = hunk.lines.maxOf { it.displayOldLineNumber }
val newHighestLineNumber = hunk.lines.maxOf { it.displayNewLineNumber }
val highestLineNumber = max(oldHighestLineNumber, newHighestLineNumber)
val highestLineNumberLength = highestLineNumber.toString().count()
hunk.lines.forEach { line ->
DiffLine(highestLineNumberLength, line)
}
}
}
}
}
}
@Composable
fun HunkHeader(
hunk: Hunk,
diffEntryType: DiffEntryType,
diffViewModel: DiffViewModel,
) {
Row(
modifier = Modifier
.background(MaterialTheme.colors.surface)
.padding(vertical = 4.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = hunk.header,
color = MaterialTheme.colors.primaryTextColor,
fontSize = 13.sp,
)
Spacer(modifier = Modifier.weight(1f))
if (
(diffEntryType is DiffEntryType.StagedDiff || diffEntryType is DiffEntryType.UnstagedDiff) &&
diffEntryType.diffEntry.changeType == DiffEntry.ChangeType.MODIFY
) {
val buttonText: String
val color: Color
if (diffEntryType is DiffEntryType.StagedDiff) {
buttonText = "Unstage hunk"
color = MaterialTheme.colors.unstageButton
} else {
buttonText = "Stage hunk"
color = MaterialTheme.colors.stageButton
}
SecondaryButton(
text = buttonText,
backgroundButton = color,
onClick = {
if (diffEntryType is DiffEntryType.StagedDiff) {
diffViewModel.unstageHunk(diffEntryType.diffEntry, hunk)
} else {
diffViewModel.stageHunk(diffEntryType.diffEntry, hunk)
}
}
)
}
}
}
@Composable
fun DiffHeader(diffEntry: DiffEntry, onCloseDiffView: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.surface),
verticalAlignment = Alignment.CenterVertically,
) {
val filePath = if (diffEntry.newPath != "/dev/null")
diffEntry.newPath
else
diffEntry.oldPath
Text(
text = filePath,
color = MaterialTheme.colors.primaryTextColor,
fontSize = 13.sp,
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.weight(1f))
IconButton(
onClick = onCloseDiffView
) {
Image(
painter = painterResource("close.svg"),
contentDescription = "Close diff",
colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryTextColor),
)
}
}
}
@Composable
fun DiffLine(highestLineNumberLength: Int, line: Line) {
val backgroundColor = when (line.lineType) {
LineType.ADDED -> {
Color(0x77a9d49b)
}
LineType.REMOVED -> {
Color(0x77dea2a2)
}
LineType.CONTEXT -> {
MaterialTheme.colors.background
}
}
Row(
modifier = Modifier
.background(backgroundColor)
) {
val oldLineText = if (line.lineType == LineType.REMOVED || line.lineType == LineType.CONTEXT) {
formattedLineNumber(line.displayOldLineNumber, highestLineNumberLength)
} else
emptyLineNumber(highestLineNumberLength)
val newLineText = if (line.lineType == LineType.ADDED || line.lineType == LineType.CONTEXT) {
formattedLineNumber(line.displayNewLineNumber, highestLineNumberLength)
} else
emptyLineNumber(highestLineNumberLength)
DisableSelection {
LineNumber(
text = oldLineText,
)
LineNumber(
text = newLineText
)
}
Text(
text = line.text,
modifier = Modifier
.padding(start = 8.dp)
.fillMaxWidth(),
color = MaterialTheme.colors.primaryTextColor,
maxLines = 1,
fontFamily = FontFamily.Monospace,
fontSize = 13.sp,
)
}
}
@Composable
fun LineNumber(text: String) {
Text(
text = text,
color = MaterialTheme.colors.primaryTextColor,
modifier = Modifier
.background(MaterialTheme.colors.surface)
.padding(horizontal = 4.dp),
fontFamily = FontFamily.Monospace,
fontSize = 13.sp,
)
}
fun formattedLineNumber(number: Int, charactersCount: Int): String {
val numberStr = number.toString()
return if (numberStr.count() == charactersCount)
numberStr
else {
val lengthDiff = charactersCount - numberStr.count()
val numberBuilder = StringBuilder()
// Add whitespaces before the numbers
repeat(lengthDiff) {
numberBuilder.append(" ")
}
numberBuilder.append(numberStr)
numberBuilder.toString()
}
}
fun emptyLineNumber(charactersCount: Int): String {
val numberBuilder = StringBuilder()
// Add whitespaces before the numbers
repeat(charactersCount) {
numberBuilder.append(" ")
}
return numberBuilder.toString()
}