Improved AsyncImage loading states.
This commit is contained in:
+118
-85
@@ -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
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user