package app.ui import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape 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.graphics.ImageBitmap import androidx.compose.ui.res.useResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.extensions.* import kotlinx.coroutines.* import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.revwalk.RevCommit import app.theme.headerBackground import app.theme.primaryTextColor import app.theme.secondaryTextColor import java.net.HttpURLConnection import java.net.URL import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.toComposeImageBitmap import app.ui.components.ScrollableLazyColumn import app.git.GitManager import app.images.ImagesCache import app.images.InMemoryImagesCache import app.theme.headerText import app.ui.components.TooltipText import org.eclipse.jgit.lib.PersonIdent import org.jetbrains.skia.Image.Companion.makeFromEncoded @Composable fun CommitChanges( gitManager: GitManager, commit: RevCommit, onDiffSelected: (DiffEntry) -> Unit ) { var diff by remember { mutableStateOf(emptyList()) } LaunchedEffect(commit) { diff = gitManager.diffListFromCommit(commit) } Column( modifier = Modifier .fillMaxSize(), ) { val scroll = rememberScrollState(0) Column( modifier = Modifier .fillMaxWidth() .padding(8.dp), ) { SelectionContainer { Text( text = commit.fullMessage, fontSize = 14.sp, modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colors.background) .height(120.dp) .padding(8.dp) .verticalScroll(scroll), ) } Divider(modifier = Modifier.fillMaxWidth()) Author(commit) } Column( modifier = Modifier .fillMaxWidth() .weight(1f, fill = true) .padding(horizontal = 8.dp, vertical = 8.dp) .background(MaterialTheme.colors.background) ) { Text( modifier = Modifier .background(MaterialTheme.colors.headerBackground) .padding(vertical = 8.dp) .fillMaxWidth(), text = "Files changed", fontWeight = FontWeight.Bold, textAlign = TextAlign.Center, color = MaterialTheme.colors.headerText, maxLines = 1, fontSize = 14.sp, ) CommitLogChanges(diff, onDiffSelected = onDiffSelected) } } } @Composable fun Author(commit: RevCommit) { val authorIdent = commit.authorIdent Row( modifier = Modifier .fillMaxWidth() .height(72.dp) .background(MaterialTheme.colors.background), verticalAlignment = Alignment.CenterVertically ) { val url = "https://www.gravatar.com/avatar/${authorIdent.emailAddress.md5}" Image( bitmap = rememberNetworkImage(url), modifier = Modifier .padding(horizontal = 16.dp) .height(40.dp) .clip(CircleShape), contentDescription = null, ) Column( modifier = Modifier .fillMaxSize(), verticalArrangement = Arrangement.Center ) { Row { TooltipText( text = authorIdent.name, color = MaterialTheme.colors.primaryTextColor, maxLines = 1, fontSize = 14.sp, tooltipTitle = authorIdent.emailAddress, ) Spacer(modifier = Modifier.weight(1f, fill = true)) val date = remember(authorIdent) { authorIdent.`when`.toSmartSystemString() } TooltipText( text = date, color = MaterialTheme.colors.secondaryTextColor, maxLines = 1, modifier = Modifier.padding(horizontal = 16.dp), fontSize = 12.sp, tooltipTitle = authorIdent.`when`.toSystemDateTimeString() ) } Text( text = commit.id.abbreviate(7).name(), color = MaterialTheme.colors.secondaryTextColor, maxLines = 1, fontSize = 12.sp, ) } } } suspend fun loadImage(link: String): ByteArray = withContext(Dispatchers.IO) { val url = URL(link) val connection = url.openConnection() as HttpURLConnection connection.connect() connection.inputStream.toByteArray() } @Composable fun rememberNetworkImage(url: String, cache: ImagesCache = InMemoryImagesCache): ImageBitmap { val cachedImage = cache.getCachedObject(url) var image by remember(url) { if(cachedImage != null) mutableStateOf(makeFromEncoded(cachedImage).toComposeImageBitmap()) else mutableStateOf( useResource("image.jpg") { makeFromEncoded(it.toByteArray()).toComposeImageBitmap() } ) } if(cachedImage == null) { LaunchedEffect(url) { try { loadImage(url).let { image = makeFromEncoded(it).toComposeImageBitmap() cache.cacheImage(url, it) } } catch (ex: Exception) { println("Avatar loading failed: ${ex.message}") } } } return image } @Composable fun CommitLogChanges(diffEntries: List, onDiffSelected: (DiffEntry) -> Unit) { val selectedIndex = remember(diffEntries) { mutableStateOf(-1) } ScrollableLazyColumn( modifier = Modifier .fillMaxSize() ) { itemsIndexed(items = diffEntries) { index, diffEntry -> val textColor = if (selectedIndex.value == index) { MaterialTheme.colors.primary } else MaterialTheme.colors.primaryTextColor Column( modifier = Modifier .height(48.dp) .fillMaxWidth() .clickable { selectedIndex.value = index onDiffSelected(diffEntry) }, verticalArrangement = Arrangement.Center, ) { Spacer(modifier = Modifier.weight(2f)) Row { Icon( modifier = Modifier .padding(start = 8.dp) .size(16.dp), imageVector = diffEntry.icon, contentDescription = null, tint = diffEntry.iconColor, ) Text( text = diffEntry.filePath, modifier = Modifier.padding(horizontal = 8.dp), color = textColor, maxLines = 1, fontSize = 14.sp, overflow = TextOverflow.Ellipsis, ) } Spacer(modifier = Modifier.weight(2f)) Divider() } } } }