Improved AsyncImage loading states.

This commit is contained in:
Vitor Pamplona
2023-06-29 15:09:08 -04:00
parent 33f915879b
commit 927ef17896
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -22,7 +21,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
@@ -42,6 +40,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -63,12 +62,12 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isFinite import androidx.compose.ui.unit.isFinite
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.core.net.toUri import androidx.core.net.toUri
import coil.annotation.ExperimentalCoilApi import coil.annotation.ExperimentalCoilApi
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.imageLoader import coil.imageLoader
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
@@ -77,6 +76,7 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
import com.vitorpamplona.amethyst.ui.note.BlankNote import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.theme.Font17SP
import com.vitorpamplona.amethyst.ui.theme.hashVerified import com.vitorpamplona.amethyst.ui.theme.hashVerified
import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.amethyst.ui.theme.imageModifier
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@@ -211,90 +211,57 @@ fun ZoomableContentView(content: ZoomableContent, images: ImmutableList<Zoomable
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun LocalImageView( private fun LocalImageView(
content: ZoomableLocalImage, content: ZoomableLocalImage,
mainImageModifier: Modifier mainImageModifier: Modifier
) { ) {
BoxWithConstraints(contentAlignment = Alignment.Center) { if (content.localFile != null && content.localFile.exists()) {
// store the dialog open or close state BoxWithConstraints(contentAlignment = Alignment.Center) {
var imageLoadingState by remember { val myModifier = remember {
mutableStateOf<Boolean?>(null) mainImageModifier
} .widthIn(max = maxWidth)
.heightIn(max = maxHeight)
.run {
aspectRatio(content.dim)?.let { ratio ->
this.aspectRatio(ratio, false)
} ?: this
}
}
val myModifier = remember { val contentScale = remember {
mainImageModifier if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth
.widthIn(max = maxWidth) }
.heightIn(max = maxHeight)
.run {
aspectRatio(content.dim)?.let { ratio ->
this.aspectRatio(ratio, false)
} ?: this
}
}
val contentScale = remember { val verifierModifier = Modifier.align(Alignment.TopEnd)
if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth
} val painterState = remember {
mutableStateOf<AsyncImagePainter.State?>(null)
}
if (content.localFile != null && content.localFile.exists()) {
AsyncImage( AsyncImage(
model = content.localFile, model = content.localFile,
contentDescription = content.description, contentDescription = content.description,
contentScale = contentScale, contentScale = contentScale,
modifier = myModifier, modifier = myModifier,
onError = { onState = {
if (imageLoadingState != false) { painterState.value = it
imageLoadingState = false
}
},
onSuccess = {
if (imageLoadingState != true) {
imageLoadingState = true
}
} }
) )
}
if (imageLoadingState == true) { AddedImageFeatures(painterState, content, contentScale, myModifier, verifierModifier)
if (content.isVerified != null) {
HashVerificationSymbol(content.isVerified, Modifier.align(Alignment.TopEnd))
}
} else if (imageLoadingState == false || content.localFile == null || !content.localFile.exists()) {
BlankNote()
} else {
if (content.blurhash != null) {
DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier)
} else {
FlowRow() {
DisplayUrlWithLoadingSymbol(content)
}
}
} }
} else {
BlankNote()
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun UrlImageView( private fun UrlImageView(
content: ZoomableUrlImage, content: ZoomableUrlImage,
mainImageModifier: Modifier mainImageModifier: Modifier
) { ) {
val scope = rememberCoroutineScope()
BoxWithConstraints(contentAlignment = Alignment.Center) { BoxWithConstraints(contentAlignment = Alignment.Center) {
val context = LocalContext.current
// store the dialog open or close state
var imageLoadingStatus by remember {
mutableStateOf<Boolean?>(null)
}
var verifiedHash by remember {
mutableStateOf<Boolean?>(null)
}
val myModifier = remember { val myModifier = remember {
mainImageModifier mainImageModifier
.widthIn(max = maxWidth) .widthIn(max = maxWidth)
@@ -310,35 +277,37 @@ private fun UrlImageView(
if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth
} }
val verifierModifier = Modifier.align(Alignment.TopEnd)
val painterState = remember {
mutableStateOf<AsyncImagePainter.State?>(null)
}
AsyncImage( AsyncImage(
model = content.url, model = content.url,
contentDescription = content.description, contentDescription = content.description,
contentScale = contentScale, contentScale = contentScale,
modifier = myModifier, modifier = myModifier,
onError = { onState = {
if (imageLoadingStatus != false) { painterState.value = it
imageLoadingStatus = false
}
},
onSuccess = {
if (verifiedHash == null) {
scope.launch(Dispatchers.IO) {
verifiedHash = verifyHash(content, context)
}
}
if (imageLoadingStatus != true) {
imageLoadingStatus = true
}
} }
) )
if (imageLoadingStatus == true) { AddedImageFeatures(painterState, content, contentScale, myModifier, verifierModifier)
verifiedHash?.let { }
HashVerificationSymbol(it, Modifier.align(Alignment.TopEnd)) }
}
} else if (imageLoadingStatus == false) { @Composable
ClickableUrl(urlText = "${content.url} ", url = content.url) @OptIn(ExperimentalLayoutApi::class)
} else { private fun AddedImageFeatures(
painter: MutableState<AsyncImagePainter.State?>,
content: ZoomableLocalImage,
contentScale: ContentScale,
myModifier: Modifier,
verifiedModifier: Modifier
) {
when (painter.value) {
null, is AsyncImagePainter.State.Loading -> {
if (content.blurhash != null) { if (content.blurhash != null) {
DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier) DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier)
} else { } else {
@@ -347,6 +316,70 @@ private fun UrlImageView(
} }
} }
} }
is AsyncImagePainter.State.Error -> {
BlankNote()
}
is AsyncImagePainter.State.Success -> {
if (content.isVerified != null) {
HashVerificationSymbol(content.isVerified, verifiedModifier)
}
}
else -> {
}
}
}
@Composable
@OptIn(ExperimentalLayoutApi::class)
private fun AddedImageFeatures(
painter: MutableState<AsyncImagePainter.State?>,
content: ZoomableUrlImage,
contentScale: ContentScale,
myModifier: Modifier,
verifiedModifier: Modifier
) {
var verifiedHash by remember {
mutableStateOf<Boolean?>(null)
}
when (painter.value) {
null, is AsyncImagePainter.State.Loading -> {
if (content.blurhash != null) {
DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier)
} else {
FlowRow() {
DisplayUrlWithLoadingSymbol(content)
}
}
}
is AsyncImagePainter.State.Error -> {
ClickableUrl(urlText = "${content.url} ", url = content.url)
}
is AsyncImagePainter.State.Success -> {
if (content.hash != null) {
val context = LocalContext.current
LaunchedEffect(key1 = content.url) {
launch(Dispatchers.IO) {
val newVerifiedHash = verifyHash(content, context)
if (newVerifiedHash != verifiedHash) {
verifiedHash = newVerifiedHash
}
}
}
}
verifiedHash?.let {
HashVerificationSymbol(it, verifiedModifier)
}
}
else -> {
}
} }
} }
@@ -382,7 +415,7 @@ private fun DisplayUrlWithLoadingSymbol(content: ZoomableContent) {
@Composable @Composable
private fun DisplayUrlWithLoadingSymbolWait(content: ZoomableContent) { private fun DisplayUrlWithLoadingSymbolWait(content: ZoomableContent) {
if (content is ZoomableUrlContent) { if (content is ZoomableUrlContent) {
ClickableUrl(urlText = "${content.url} ", url = content.url) ClickableUrl(urlText = remember { "${content.url} " }, url = content.url)
} else { } else {
Text("Loading content... ") Text("Loading content... ")
} }
@@ -401,8 +434,8 @@ private fun DisplayUrlWithLoadingSymbolWait(content: ZoomableContent) {
myId, myId,
InlineTextContent( InlineTextContent(
Placeholder( Placeholder(
width = 17.sp, width = Font17SP,
height = 17.sp, height = Font17SP,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center placeholderVerticalAlign = PlaceholderVerticalAlign.Center
) )
) { ) {