364 lines
11 KiB
Kotlin
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()
|
|
}
|
|
|