diff --git a/src/main/kotlin/app/extensions/SemaphoreExtensions.kt b/src/main/kotlin/app/extensions/SemaphoreExtensions.kt new file mode 100644 index 0000000..ac90875 --- /dev/null +++ b/src/main/kotlin/app/extensions/SemaphoreExtensions.kt @@ -0,0 +1,15 @@ +package app.extensions + +import androidx.compose.runtime.Composable +import kotlinx.coroutines.sync.Semaphore + +suspend inline fun Semaphore.acquireAndUse( + block: () -> Composable +) { + this.acquire() + try { + block() + } finally { + this.release() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/images/ImagesCache.kt b/src/main/kotlin/app/images/ImagesCache.kt index d8ef842..90c6d04 100644 --- a/src/main/kotlin/app/images/ImagesCache.kt +++ b/src/main/kotlin/app/images/ImagesCache.kt @@ -1,6 +1,6 @@ package app.images interface ImagesCache { - fun getCachedObject(urlSource: String): ByteArray? + fun getCachedImage(urlSource: String): ByteArray? fun cacheImage(urlSource: String, image: ByteArray) } \ No newline at end of file diff --git a/src/main/kotlin/app/images/InMemoryImagesCache.kt b/src/main/kotlin/app/images/InMemoryImagesCache.kt index 7369e3d..73e3a1a 100644 --- a/src/main/kotlin/app/images/InMemoryImagesCache.kt +++ b/src/main/kotlin/app/images/InMemoryImagesCache.kt @@ -3,7 +3,7 @@ package app.images object InMemoryImagesCache : ImagesCache { private val cachedImages = hashMapOf() - override fun getCachedObject(urlSource: String): ByteArray? { + override fun getCachedImage(urlSource: String): ByteArray? { return cachedImages[urlSource] } diff --git a/src/main/kotlin/app/images/NetworkImageLoader.kt b/src/main/kotlin/app/images/NetworkImageLoader.kt new file mode 100644 index 0000000..0fffc31 --- /dev/null +++ b/src/main/kotlin/app/images/NetworkImageLoader.kt @@ -0,0 +1,75 @@ +package app.images + +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import androidx.compose.ui.res.useResource +import app.extensions.acquireAndUse +import app.extensions.toByteArray +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.withContext +import org.jetbrains.skia.Image +import java.net.HttpURLConnection +import java.net.URL + +private const val MAX_LOADING_IMAGES = 3 + +object NetworkImageLoader { + private val loadingImagesSemaphore = Semaphore(MAX_LOADING_IMAGES) + private val cache: ImagesCache = InMemoryImagesCache + + suspend fun loadImageNetwork(url: String): ImageBitmap? = withContext(Dispatchers.IO) { + try { + val cachedImage = cache.getCachedImage(url) + + if(cachedImage != null) + return@withContext cachedImage.toComposeImage() + + loadingImagesSemaphore.acquireAndUse { + val imageByteArray = loadImage(url) + cache.cacheImage(url, imageByteArray) + return@withContext imageByteArray.toComposeImage() + } + + } catch (ex: Exception) { + ex.printStackTrace() + } + + // If a previous return hasn't been called, something has gone wrong, return null + return@withContext null + } + + 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): ImageBitmap { + val networkImageLoader = NetworkImageLoader + var image by remember(url) { + mutableStateOf( + useResource("image.jpg") { + Image.makeFromEncoded(it.toByteArray()).toComposeImageBitmap() + } + ) + } + + LaunchedEffect(url) { + val networkImage = networkImageLoader.loadImageNetwork(url) + if(networkImage != null) + image = networkImage + } + + return image +} + +fun ByteArray.toComposeImage() = Image.makeFromEncoded(this).toComposeImageBitmap() + diff --git a/src/main/kotlin/app/ui/CommitChanges.kt b/src/main/kotlin/app/ui/CommitChanges.kt index f2e8744..fc1f54e 100644 --- a/src/main/kotlin/app/ui/CommitChanges.kt +++ b/src/main/kotlin/app/ui/CommitChanges.kt @@ -5,38 +5,30 @@ 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.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text 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.git.GitManager +import app.images.rememberNetworkImage import app.theme.headerBackground +import app.theme.headerText 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 +import org.eclipse.jgit.diff.DiffEntry +import org.eclipse.jgit.revwalk.RevCommit @Composable fun CommitChanges( @@ -166,45 +158,6 @@ fun Author(commit: RevCommit) { } } -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) } diff --git a/src/main/kotlin/app/ui/log/Log.kt b/src/main/kotlin/app/ui/log/Log.kt index 4a03a59..8eed9bb 100644 --- a/src/main/kotlin/app/ui/log/Log.kt +++ b/src/main/kotlin/app/ui/log/Log.kt @@ -36,6 +36,7 @@ import app.extensions.* import app.git.GitManager import app.git.LogStatus import app.git.graph.GraphNode +import app.images.rememberNetworkImage import app.theme.headerBackground import app.theme.headerText import app.theme.primaryTextColor @@ -46,7 +47,6 @@ import app.ui.dialogs.MergeDialog import app.ui.dialogs.NewBranchDialog import app.ui.dialogs.NewTagDialog import app.ui.dialogs.ResetBranchDialog -import app.ui.rememberNetworkImage import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.revwalk.RevCommit