diff --git a/build.gradle.kts b/build.gradle.kts index 3b6e99e..b6aa14c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) implementation(compose.desktop.components.splitPane) implementation(compose("org.jetbrains.compose.ui:ui-util")) + implementation(compose("org.jetbrains.compose.components:components-animatedimage")) implementation("org.eclipse.jgit:org.eclipse.jgit:6.3.0.202209071007-r") implementation("org.apache.sshd:sshd-core:2.9.0") implementation("com.google.dagger:dagger:2.43.2") diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt index 85ac034..eafb96d 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt @@ -12,23 +12,23 @@ import org.eclipse.jgit.treewalk.AbstractTreeIterator import org.eclipse.jgit.treewalk.WorkingTreeIterator import org.eclipse.jgit.util.LfsFactory import java.io.FileOutputStream +import java.nio.file.Files import java.nio.file.Path import javax.inject.Inject import kotlin.io.path.createTempFile private const val DEFAULT_BINARY_FILE_THRESHOLD = PackConfig.DEFAULT_BIG_FILE_THRESHOLD +private const val IMAGE_CONTENT_TYPE = "image/" + +val animatedImages = arrayOf( + "image/gif", + "image/webp" +) class RawFileManager @Inject constructor( private val tempFilesManager: TempFilesManager, ) { - private val imageFormatsSupported = listOf( - "png", - "jpg", - "jpeg", - "webp", - ) - private fun source(iterator: AbstractTreeIterator, reader: ObjectReader): ContentSource { return if (iterator is WorkingTreeIterator) ContentSource.create(iterator) @@ -86,15 +86,14 @@ class RawFileManager @Inject constructor( ldr.copyTo(out) } - return EntryContent.ImageBinary(tempFile) + return EntryContent.ImageBinary(tempFile, Files.probeContentType(Path.of(entry.newPath)).orEmpty()) } - // todo check if it's an image checking the binary format, checking the extension is a temporary workaround private fun isImage(entry: DiffEntry): Boolean { val path = entry.newPath - val fileExtension = path.split(".").lastOrNull() ?: return false + val contentType = Files.probeContentType(Path.of(path)) - return imageFormatsSupported.contains(fileExtension.lowercase()) + return contentType?.startsWith(IMAGE_CONTENT_TYPE) ?: false } } @@ -103,7 +102,7 @@ sealed class EntryContent { object InvalidObjectBlob : EntryContent() data class Text(val rawText: RawText) : EntryContent() sealed class BinaryContent : EntryContent() - data class ImageBinary(val tempFilePath: Path) : BinaryContent() + data class ImageBinary(val tempFilePath: Path, val contentType: String) : BinaryContent() object Binary : BinaryContent() object TooLargeEntry : EntryContent() } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt index 71cd1ab..8fd2016 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerIconDefaults @@ -30,6 +31,10 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.jetpackduba.gitnuro.extensions.* +import com.jetpackduba.gitnuro.git.DiffEntryType +import com.jetpackduba.gitnuro.git.EntryContent +import com.jetpackduba.gitnuro.git.animatedImages import com.jetpackduba.gitnuro.git.diff.DiffResult import com.jetpackduba.gitnuro.git.diff.Hunk import com.jetpackduba.gitnuro.git.diff.Line @@ -38,16 +43,19 @@ import com.jetpackduba.gitnuro.git.workspace.StatusEntry import com.jetpackduba.gitnuro.git.workspace.StatusType import com.jetpackduba.gitnuro.keybindings.KeybindingOption import com.jetpackduba.gitnuro.keybindings.matchesBinding +import com.jetpackduba.gitnuro.theme.* import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn import com.jetpackduba.gitnuro.ui.components.SecondaryButton import com.jetpackduba.gitnuro.viewmodels.DiffViewModel import com.jetpackduba.gitnuro.viewmodels.TextDiffType import com.jetpackduba.gitnuro.viewmodels.ViewDiffResult -import com.jetpackduba.gitnuro.extensions.* -import com.jetpackduba.gitnuro.git.DiffEntryType -import com.jetpackduba.gitnuro.git.EntryContent -import com.jetpackduba.gitnuro.theme.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.eclipse.jgit.diff.DiffEntry +import org.jetbrains.compose.animatedimage.Blank +import org.jetbrains.compose.animatedimage.animate +import org.jetbrains.compose.animatedimage.loadAnimatedImage +import org.jetbrains.compose.resources.loadOrNull import java.io.FileInputStream import java.nio.file.Path import kotlin.io.path.absolutePathString @@ -206,7 +214,7 @@ fun NonTextDiff(diffResult: DiffResult.NonText) { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - SideTitle("Binary file") +// SideTitle("Binary file") Spacer(modifier = Modifier.height(24.dp)) SideDiff(newBinaryContent) } @@ -227,7 +235,7 @@ fun SideTitle(text: String) { fun SideDiff(entryContent: EntryContent) { when (entryContent) { EntryContent.Binary -> BinaryDiff() - is EntryContent.ImageBinary -> ImageDiff(entryContent.tempFilePath) + is EntryContent.ImageBinary -> ImageDiff(entryContent.tempFilePath, entryContent.contentType) else -> { } // is EntryContent.Text -> //TODO maybe have a text view if the file was a binary before? @@ -236,13 +244,46 @@ fun SideDiff(entryContent: EntryContent) { } @Composable -fun ImageDiff(tempImagePath: Path) { +private fun ImageDiff(tempImagePath: Path, contentType: String) { + val imagePath = tempImagePath.absolutePathString() + + if(animatedImages.contains(contentType)) { + AnimatedImage(imagePath) + } else { + StaticImage(imagePath) + } +} + +@Composable +private fun StaticImage(tempImagePath: String) { + var image by remember(tempImagePath) { mutableStateOf(null) } + + LaunchedEffect(tempImagePath) { + withContext(Dispatchers.IO) { + FileInputStream(tempImagePath).use { inputStream -> + image = loadImageBitmap(inputStream = inputStream) + } + } + } + Image( - bitmap = loadImageBitmap(inputStream = FileInputStream(tempImagePath.absolutePathString())), + bitmap = image ?: ImageBitmap.Blank, contentDescription = null, modifier = Modifier.fillMaxSize() .handMouseClickable { - openFileWithExternalApp(tempImagePath.absolutePathString()) + openFileWithExternalApp(tempImagePath) + } + ) +} + +@Composable +private fun AnimatedImage(tempImagePath: String) { + Image( + bitmap = loadOrNull(tempImagePath) { loadAnimatedImage(tempImagePath) }?.animate() ?: ImageBitmap.Blank, + contentDescription = null, + modifier = Modifier.fillMaxSize() + .handMouseClickable { + openFileWithExternalApp(tempImagePath) } ) }