Gitnuro/src/main/kotlin/com/jetpackduba/gitnuro/ui/CommitChanges.kt
2023-07-15 13:21:19 +02:00

385 lines
14 KiB
Kotlin

package com.jetpackduba.gitnuro.ui
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
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.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.extensions.*
import com.jetpackduba.gitnuro.git.DiffEntryType
import com.jetpackduba.gitnuro.theme.*
import com.jetpackduba.gitnuro.ui.components.*
import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu
import com.jetpackduba.gitnuro.ui.context_menu.commitedChangesEntriesContextMenuItems
import com.jetpackduba.gitnuro.viewmodels.CommitChangesState
import com.jetpackduba.gitnuro.viewmodels.CommitChangesViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.lib.PersonIdent
import org.eclipse.jgit.revwalk.RevCommit
@Composable
fun CommitChanges(
commitChangesViewModel: CommitChangesViewModel = gitnuroViewModel(),
selectedItem: SelectedItem.CommitBasedItem,
onDiffSelected: (DiffEntry) -> Unit,
diffSelected: DiffEntryType?,
onBlame: (String) -> Unit,
onHistory: (String) -> Unit,
) {
LaunchedEffect(selectedItem) {
commitChangesViewModel.loadChanges(selectedItem.revCommit)
}
val commitChangesStatus = commitChangesViewModel.commitChangesState.collectAsState().value
val showSearch by commitChangesViewModel.showSearch.collectAsState()
val changesListScroll by commitChangesViewModel.changesLazyListState.collectAsState()
val textScroll by commitChangesViewModel.textScroll.collectAsState()
var searchFilter by remember(commitChangesViewModel, showSearch, commitChangesStatus) {
mutableStateOf(commitChangesViewModel.searchFilter.value)
}
when (commitChangesStatus) {
CommitChangesState.Loading -> {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant)
}
is CommitChangesState.Loaded -> {
CommitChangesView(
diffSelected = diffSelected,
commit = commitChangesStatus.commit,
changes = commitChangesStatus.changesFiltered,
onBlame = onBlame,
onHistory = onHistory,
showSearch = showSearch,
changesListScroll = changesListScroll,
textScroll = textScroll,
searchFilter = searchFilter,
onDiffSelected = onDiffSelected,
onSearchFilterToggled = { visible ->
commitChangesViewModel.onSearchFilterToggled(visible)
},
onSearchFilterChanged = { filter ->
searchFilter = filter
commitChangesViewModel.onSearchFilterChanged(filter)
},
)
}
}
}
@Composable
fun CommitChangesView(
commit: RevCommit,
changes: List<DiffEntry>,
diffSelected: DiffEntryType?,
changesListScroll: LazyListState,
textScroll: ScrollState,
showSearch: Boolean,
searchFilter: TextFieldValue,
onBlame: (String) -> Unit,
onHistory: (String) -> Unit,
onDiffSelected: (DiffEntry) -> Unit,
onSearchFilterToggled: (Boolean) -> Unit,
onSearchFilterChanged: (TextFieldValue) -> Unit,
) {
/**
* State used to prevent the text field from getting the focus when returning from another tab
*/
var requestFocus by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.padding(end = 8.dp, bottom = 8.dp)
.fillMaxSize(),
) {
val searchFocusRequester = remember { FocusRequester() }
Column(
modifier = Modifier
.padding(bottom = 4.dp)
.fillMaxWidth()
.weight(1f, fill = true)
.background(MaterialTheme.colors.background)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(34.dp)
.background(MaterialTheme.colors.tertiarySurface),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 16.dp),
text = "Files changed",
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Left,
color = MaterialTheme.colors.onBackground,
maxLines = 1,
style = MaterialTheme.typography.body2,
)
Box(modifier = Modifier.weight(1f))
IconButton(
onClick = {
onSearchFilterToggled(!showSearch)
if (!showSearch)
requestFocus = true
},
modifier = Modifier.handOnHover(),
) {
Icon(
painter = painterResource(AppIcons.SEARCH),
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colors.onBackground,
)
}
}
if (showSearch) {
SearchTextField(
searchFilter = searchFilter,
onSearchFilterChanged = onSearchFilterChanged,
searchFocusRequester = searchFocusRequester,
onClose = { onSearchFilterToggled(false) },
)
}
LaunchedEffect(showSearch, requestFocus) {
if (showSearch && requestFocus) {
searchFocusRequester.requestFocus()
requestFocus = false
}
}
CommitLogChanges(
diffSelected = diffSelected,
changesListScroll = changesListScroll,
diffEntries = changes,
onDiffSelected = onDiffSelected,
onBlame = onBlame,
onHistory = onHistory,
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colors.background),
) {
SelectionContainer {
Text(
text = commit.fullMessage,
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground,
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.padding(8.dp)
.verticalScroll(textScroll),
)
}
Author(commit.shortName, commit.name, commit.authorIdent)
}
}
}
@Composable
fun Author(
shortName: String,
name: String,
author: PersonIdent,
) {
var copied by remember(name) { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val clipboard = LocalClipboardManager.current
Row(
modifier = Modifier
.fillMaxWidth()
.height(72.dp)
.background(MaterialTheme.colors.tertiarySurface),
verticalAlignment = Alignment.CenterVertically
) {
AvatarImage(
modifier = Modifier
.padding(horizontal = 16.dp)
.size(40.dp),
personIdent = author,
)
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center
) {
TooltipText(
text = author.name,
maxLines = 1,
style = MaterialTheme.typography.body2,
tooltipTitle = author.emailAddress,
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = shortName,
color = MaterialTheme.colors.onBackgroundSecondary,
maxLines = 1,
style = MaterialTheme.typography.body2,
modifier = Modifier.handMouseClickable {
scope.launch {
clipboard.setText(AnnotatedString(name))
copied = true
delay(2000) // 2s
copied = false
}
}
)
if (copied) {
Text(
text = "Copied!",
color = MaterialTheme.colors.primaryVariant,
maxLines = 1,
style = MaterialTheme.typography.caption,
modifier = Modifier.padding(start = 4.dp),
)
}
Spacer(modifier = Modifier.weight(1f, fill = true))
val smartDate = remember(author) {
author.`when`.toSmartSystemString()
}
val systemDate = remember(author) {
author.`when`.toSystemDateTimeString()
}
TooltipText(
text = smartDate,
color = MaterialTheme.colors.onBackgroundSecondary,
maxLines = 1,
modifier = Modifier.padding(horizontal = 16.dp),
style = MaterialTheme.typography.body2,
tooltipTitle = systemDate
)
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CommitLogChanges(
diffEntries: List<DiffEntry>,
diffSelected: DiffEntryType?,
changesListScroll: LazyListState,
onBlame: (String) -> Unit,
onHistory: (String) -> Unit,
onDiffSelected: (DiffEntry) -> Unit,
) {
ScrollableLazyColumn(
modifier = Modifier
.fillMaxSize(),
state = changesListScroll,
) {
items(items = diffEntries) { diffEntry ->
ContextMenu(
items = {
commitedChangesEntriesContextMenuItems(
diffEntry,
onBlame = { onBlame(diffEntry.filePath) },
onHistory = { onHistory(diffEntry.filePath) },
)
}
) {
Column(
modifier = Modifier
.height(40.dp)
.fillMaxWidth()
.handMouseClickable {
onDiffSelected(diffEntry)
}
.backgroundIf(
condition = diffSelected is DiffEntryType.CommitDiff && diffSelected.diffEntry == diffEntry,
color = MaterialTheme.colors.backgroundSelected,
),
verticalArrangement = Arrangement.Center,
) {
Spacer(modifier = Modifier.weight(2f))
Row {
Icon(
modifier = Modifier
.padding(horizontal = 8.dp)
.size(16.dp),
imageVector = diffEntry.icon,
contentDescription = null,
tint = diffEntry.iconColor,
)
if (diffEntry.parentDirectoryPath.isNotEmpty()) {
Text(
text = diffEntry.parentDirectoryPath.removeSuffix("/"),
modifier = Modifier.weight(1f, fill = false),
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackgroundSecondary,
)
Text(
text = "/",
maxLines = 1,
softWrap = false,
style = MaterialTheme.typography.body2,
overflow = TextOverflow.Visible,
color = MaterialTheme.colors.onBackgroundSecondary,
)
}
Text(
text = diffEntry.fileName,
maxLines = 1,
softWrap = false,
modifier = Modifier.padding(end = 16.dp),
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackground,
)
}
Spacer(modifier = Modifier.weight(2f))
}
}
}
}
}