Improves the rendering of inline metadata
This commit is contained in:
@@ -104,8 +104,8 @@ class RichTextParser() {
|
||||
url = fullUrl,
|
||||
description = frags["alt"]?.let { URLDecoder.decode(it, "UTF-8") },
|
||||
hash = frags["x"]?.let { URLDecoder.decode(it, "UTF-8") },
|
||||
dim = frags["blurhash"]?.let { URLDecoder.decode(it, "UTF-8") },
|
||||
uri = frags["dim"]?.let { URLDecoder.decode(it, "UTF-8") }
|
||||
blurhash = frags["blurhash"]?.let { URLDecoder.decode(it, "UTF-8") },
|
||||
dim = frags["dim"]?.let { URLDecoder.decode(it, "UTF-8") }
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
||||
@@ -2,11 +2,17 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.ImageDecoder
|
||||
import android.media.MediaDataSource
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.media.MediaMetadataRetriever.BitmapParams
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import io.trbl.blurhash.BlurHash
|
||||
import java.io.IOException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class FileHeader(
|
||||
@@ -76,8 +82,42 @@ class FileHeader(
|
||||
} else {
|
||||
Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 4, 4), dim)
|
||||
}
|
||||
} else if (mimeType?.startsWith("video/") == true) {
|
||||
val mediaMetadataRetriever = MediaMetadataRetriever()
|
||||
mediaMetadataRetriever.setDataSource(ByteArrayMediaDataSource(data))
|
||||
|
||||
val newDim = mediaMetadataRetriever.prepareDimFromVideo() ?: dimPrecomputed
|
||||
|
||||
val blurhash = mediaMetadataRetriever.getThumbnail()?.let { thumbnail ->
|
||||
val aspectRatio = (thumbnail.width).toFloat() / (thumbnail.height).toFloat()
|
||||
|
||||
val intArray = IntArray(thumbnail.width * thumbnail.height)
|
||||
thumbnail.getPixels(
|
||||
intArray,
|
||||
0,
|
||||
thumbnail.width,
|
||||
0,
|
||||
0,
|
||||
thumbnail.width,
|
||||
thumbnail.height
|
||||
)
|
||||
|
||||
if (aspectRatio > 1) {
|
||||
BlurHash.encode(intArray, thumbnail.width, thumbnail.height, 9, (9 * (1 / aspectRatio)).roundToInt())
|
||||
} else if (aspectRatio < 1) {
|
||||
BlurHash.encode(intArray, thumbnail.width, thumbnail.height, (9 * aspectRatio).roundToInt(), 9)
|
||||
} else {
|
||||
BlurHash.encode(intArray, thumbnail.width, thumbnail.height, 4, 4)
|
||||
}
|
||||
}
|
||||
|
||||
if (newDim != "0x0") {
|
||||
Pair(blurhash, newDim)
|
||||
} else {
|
||||
Pair(blurhash, null)
|
||||
}
|
||||
} else {
|
||||
Pair(null, dimPrecomputed)
|
||||
Pair(null, null)
|
||||
}
|
||||
|
||||
onReady(FileHeader(mimeType, hash, size, dim, blurHash))
|
||||
@@ -88,3 +128,79 @@ class FileHeader(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun MediaMetadataRetriever.getThumbnail(): Bitmap? {
|
||||
val raw: ByteArray? = getEmbeddedPicture()
|
||||
if (raw != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
return ImageDecoder.decodeBitmap(ImageDecoder.createSource(raw))
|
||||
}
|
||||
}
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val params = BitmapParams()
|
||||
params.preferredConfig = Bitmap.Config.ARGB_8888
|
||||
|
||||
// Fall back to middle of video
|
||||
// Note: METADATA_KEY_DURATION unit is in ms, not us.
|
||||
val thumbnailTimeUs: Long = (extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0) * 1000 / 2
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
getFrameAtTime(thumbnailTimeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, params)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun MediaMetadataRetriever.prepareDimFromVideo(): String? {
|
||||
val width = prepareVideoWidth() ?: return null
|
||||
val height = prepareVideoHeight() ?: return null
|
||||
|
||||
return "${width}x$height"
|
||||
}
|
||||
|
||||
fun MediaMetadataRetriever.prepareVideoWidth(): Int? {
|
||||
val widthData = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
|
||||
return if (widthData.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
widthData.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun MediaMetadataRetriever.prepareVideoHeight(): Int? {
|
||||
val heightData = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
|
||||
return if (heightData.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
heightData.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
class ByteArrayMediaDataSource(var imageData: ByteArray) : MediaDataSource() {
|
||||
override fun getSize(): Long {
|
||||
return imageData.size.toLong()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int {
|
||||
if (position >= imageData.size) {
|
||||
return -1
|
||||
}
|
||||
val newSize = if (position + size > imageData.size) {
|
||||
size - ((position.toInt() + size) - imageData.size)
|
||||
} else {
|
||||
size
|
||||
}
|
||||
|
||||
imageData.copyInto(buffer, offset, position.toInt(), position.toInt() + newSize)
|
||||
|
||||
return newSize
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -44,6 +45,8 @@ import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
@@ -160,6 +163,8 @@ fun VideoView(
|
||||
waveform: ImmutableList<Int>? = null,
|
||||
artworkUri: String? = null,
|
||||
authorName: String? = null,
|
||||
dimensions: String? = null,
|
||||
blurhash: String? = null,
|
||||
nostrUriCallback: String? = null,
|
||||
onDialog: ((Boolean) -> Unit)? = null,
|
||||
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
||||
@@ -168,22 +173,42 @@ fun VideoView(
|
||||
) {
|
||||
val defaultToStart by remember(videoUri) { mutableStateOf(DefaultMutedSetting.value) }
|
||||
|
||||
VideoViewInner(
|
||||
videoUri = videoUri,
|
||||
defaultToStart = defaultToStart,
|
||||
title = title,
|
||||
thumb = thumb,
|
||||
roundedCorner = roundedCorner,
|
||||
topPaddingForControllers = topPaddingForControllers,
|
||||
waveform = waveform,
|
||||
artworkUri = artworkUri,
|
||||
authorName = authorName,
|
||||
nostrUriCallback = nostrUriCallback,
|
||||
alwaysShowVideo = alwaysShowVideo,
|
||||
accountViewModel = accountViewModel,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
onDialog = onDialog
|
||||
)
|
||||
val automaticallyStartPlayback = remember {
|
||||
mutableStateOf<Boolean>(
|
||||
if (alwaysShowVideo) true else accountViewModel.settings.startVideoPlayback.value
|
||||
)
|
||||
}
|
||||
|
||||
if (!automaticallyStartPlayback.value) {
|
||||
ImageUrlWithDownloadButton(url = videoUri, showImage = automaticallyStartPlayback)
|
||||
} else {
|
||||
val ratio = aspectRatio(dimensions)
|
||||
val modifier = if (ratio != null && roundedCorner) {
|
||||
Modifier.aspectRatio(ratio)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Box(modifier) {
|
||||
VideoViewInner(
|
||||
videoUri = videoUri,
|
||||
defaultToStart = defaultToStart,
|
||||
title = title,
|
||||
thumb = thumb,
|
||||
roundedCorner = roundedCorner,
|
||||
topPaddingForControllers = topPaddingForControllers,
|
||||
waveform = waveform,
|
||||
artworkUri = artworkUri,
|
||||
authorName = authorName,
|
||||
dimensions = dimensions,
|
||||
blurhash = blurhash,
|
||||
nostrUriCallback = nostrUriCallback,
|
||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
onDialog = onDialog
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -198,68 +223,61 @@ fun VideoViewInner(
|
||||
waveform: ImmutableList<Int>? = null,
|
||||
artworkUri: String? = null,
|
||||
authorName: String? = null,
|
||||
dimensions: String? = null,
|
||||
blurhash: String? = null,
|
||||
nostrUriCallback: String? = null,
|
||||
alwaysShowVideo: Boolean = false,
|
||||
accountViewModel: AccountViewModel,
|
||||
automaticallyStartPlayback: State<Boolean>,
|
||||
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
||||
onDialog: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
val automaticallyStartPlayback = remember {
|
||||
mutableStateOf<Boolean>(
|
||||
if (alwaysShowVideo) true else accountViewModel.settings.startVideoPlayback.value
|
||||
)
|
||||
}
|
||||
|
||||
if (!automaticallyStartPlayback.value) {
|
||||
ImageUrlWithDownloadButton(url = videoUri, showImage = automaticallyStartPlayback)
|
||||
} else {
|
||||
VideoPlayerActiveMutex(videoUri) { modifier, activeOnScreen ->
|
||||
val mediaItem = remember(videoUri) {
|
||||
mutableStateOf(
|
||||
MediaItem.Builder()
|
||||
.setMediaId(videoUri)
|
||||
.setUri(videoUri)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setArtist(authorName?.ifBlank { null })
|
||||
.setTitle(title?.ifBlank { null } ?: videoUri)
|
||||
.setArtworkUri(
|
||||
try {
|
||||
if (artworkUri != null) {
|
||||
Uri.parse(artworkUri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
VideoPlayerActiveMutex(videoUri) { modifier, activeOnScreen ->
|
||||
val mediaItem = remember(videoUri) {
|
||||
mutableStateOf(
|
||||
MediaItem.Builder()
|
||||
.setMediaId(videoUri)
|
||||
.setUri(videoUri)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setArtist(authorName?.ifBlank { null })
|
||||
.setTitle(title?.ifBlank { null } ?: videoUri)
|
||||
.setArtworkUri(
|
||||
try {
|
||||
if (artworkUri != null) {
|
||||
Uri.parse(artworkUri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
GetVideoController(
|
||||
mediaItem = mediaItem,
|
||||
videoUri = videoUri,
|
||||
defaultToStart = defaultToStart,
|
||||
nostrUriCallback = nostrUriCallback
|
||||
) { controller, keepPlaying ->
|
||||
RenderVideoPlayer(
|
||||
controller = controller,
|
||||
thumbData = thumb,
|
||||
roundedCorner = roundedCorner,
|
||||
topPaddingForControllers = topPaddingForControllers,
|
||||
waveform = waveform,
|
||||
keepPlaying = keepPlaying,
|
||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||
activeOnScreen = activeOnScreen,
|
||||
modifier = modifier,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
onDialog = onDialog
|
||||
)
|
||||
}
|
||||
GetVideoController(
|
||||
mediaItem = mediaItem,
|
||||
videoUri = videoUri,
|
||||
defaultToStart = defaultToStart,
|
||||
nostrUriCallback = nostrUriCallback
|
||||
) { controller, keepPlaying ->
|
||||
RenderVideoPlayer(
|
||||
controller = controller,
|
||||
thumbData = thumb,
|
||||
roundedCorner = roundedCorner,
|
||||
dimensions = dimensions,
|
||||
blurhash = blurhash,
|
||||
topPaddingForControllers = topPaddingForControllers,
|
||||
waveform = waveform,
|
||||
keepPlaying = keepPlaying,
|
||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||
activeOnScreen = activeOnScreen,
|
||||
modifier = modifier,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
onDialog = onDialog
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,6 +552,8 @@ private fun RenderVideoPlayer(
|
||||
controller: MediaController,
|
||||
thumbData: VideoThumb?,
|
||||
roundedCorner: Boolean,
|
||||
dimensions: String? = null,
|
||||
blurhash: String? = null,
|
||||
topPaddingForControllers: Dp = Dp.Unspecified,
|
||||
waveform: ImmutableList<Int>? = null,
|
||||
keepPlaying: MutableState<Boolean>,
|
||||
@@ -586,6 +606,8 @@ private fun RenderVideoPlayer(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
setBackgroundColor(Color.Transparent.toArgb())
|
||||
setShutterBackgroundColor(Color.Transparent.toArgb())
|
||||
controllerAutoShow = false
|
||||
thumbData?.thumb?.let { defaultArtwork = it }
|
||||
hideController()
|
||||
@@ -609,6 +631,19 @@ private fun RenderVideoPlayer(
|
||||
}
|
||||
}
|
||||
|
||||
val ratio = remember {
|
||||
aspectRatio(dimensions)
|
||||
}
|
||||
|
||||
if (ratio != null) {
|
||||
DisplayBlurHash(
|
||||
blurhash,
|
||||
null,
|
||||
ContentScale.Crop,
|
||||
myModifier.aspectRatio(ratio)
|
||||
)
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
modifier = myModifier,
|
||||
factory = factory
|
||||
|
||||
@@ -28,6 +28,8 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
@@ -77,6 +79,7 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.ViewCompat
|
||||
import coil.annotation.ExperimentalCoilApi
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.AsyncImagePainter
|
||||
@@ -145,7 +148,8 @@ class ZoomableUrlVideo(
|
||||
dim: String? = null,
|
||||
uri: String? = null,
|
||||
val artworkUri: String? = null,
|
||||
val authorName: String? = null
|
||||
val authorName: String? = null,
|
||||
val blurhash: String? = null
|
||||
) : ZoomableUrlContent(url, description, hash, dim, uri)
|
||||
|
||||
@Immutable
|
||||
@@ -246,6 +250,8 @@ fun ZoomableContentView(
|
||||
title = content.description,
|
||||
artworkUri = content.artworkUri,
|
||||
authorName = content.authorName,
|
||||
dimensions = content.dim,
|
||||
blurhash = content.blurhash,
|
||||
roundedCorner = roundedCorner,
|
||||
nostrUriCallback = content.uri,
|
||||
onDialog = { dialogOpen = true },
|
||||
@@ -569,8 +575,9 @@ private fun AddedImageFeatures(
|
||||
}
|
||||
}
|
||||
|
||||
private fun aspectRatio(dim: String?): Float? {
|
||||
fun aspectRatio(dim: String?): Float? {
|
||||
if (dim == null) return null
|
||||
if (dim == "0x0") return null
|
||||
|
||||
val parts = dim.split("x")
|
||||
if (parts.size != 2) return null
|
||||
@@ -578,7 +585,12 @@ private fun aspectRatio(dim: String?): Float? {
|
||||
return try {
|
||||
val width = parts[0].toFloat()
|
||||
val height = parts[1].toFloat()
|
||||
width / height
|
||||
|
||||
if (width < 0.1 || height < 0.1) {
|
||||
null
|
||||
} else {
|
||||
width / height
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
@@ -664,7 +676,7 @@ private fun InlineLoadingIcon() =
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayBlurHash(
|
||||
fun DisplayBlurHash(
|
||||
blurhash: String?,
|
||||
description: String?,
|
||||
contentScale: ContentScale,
|
||||
@@ -694,7 +706,6 @@ fun ZoomableImageDialog(
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val orientation = LocalConfiguration.current.orientation
|
||||
println("This Log only exists to force orientation listener $orientation")
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -704,6 +715,8 @@ fun ZoomableImageDialog(
|
||||
)
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val insets = ViewCompat.getRootWindowInsets(view)
|
||||
|
||||
val orientation = LocalConfiguration.current.orientation
|
||||
println("This Log only exists to force orientation listener $orientation")
|
||||
|
||||
@@ -810,6 +823,7 @@ private fun DialogContent(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.statusBarsPadding().systemBarsPadding()
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -947,6 +961,10 @@ private fun RenderImageOrVideo(
|
||||
onToggleControllerVisibility: (() -> Unit)? = null,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val automaticallyStartPlayback = remember {
|
||||
mutableStateOf<Boolean>(true)
|
||||
}
|
||||
|
||||
if (content is ZoomableUrlImage) {
|
||||
val mainModifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -968,16 +986,15 @@ private fun RenderImageOrVideo(
|
||||
)
|
||||
} else if (content is ZoomableUrlVideo) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
|
||||
VideoView(
|
||||
VideoViewInner(
|
||||
videoUri = content.url,
|
||||
title = content.description,
|
||||
artworkUri = content.artworkUri,
|
||||
authorName = content.authorName,
|
||||
roundedCorner = roundedCorner,
|
||||
topPaddingForControllers = topPaddingForControllers,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
accountViewModel = accountViewModel,
|
||||
alwaysShowVideo = true
|
||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged
|
||||
)
|
||||
}
|
||||
} else if (content is ZoomableLocalImage) {
|
||||
@@ -1002,16 +1019,15 @@ private fun RenderImageOrVideo(
|
||||
} else if (content is ZoomableLocalVideo) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
|
||||
content.localFile?.let {
|
||||
VideoView(
|
||||
VideoViewInner(
|
||||
videoUri = it.toUri().toString(),
|
||||
title = content.description,
|
||||
artworkUri = content.artworkUri,
|
||||
authorName = content.authorName,
|
||||
roundedCorner = roundedCorner,
|
||||
topPaddingForControllers = topPaddingForControllers,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||
accountViewModel = accountViewModel,
|
||||
alwaysShowVideo = true
|
||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||
onControllerVisibilityChanged = onControllerVisibilityChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, showHi
|
||||
}
|
||||
Log.d("Rendering Metrics", "All Galeries: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed - ")
|
||||
|
||||
Row(remember { Modifier.fillMaxWidth() }) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Spacer(modifier = WidthAuthorPictureModifierWithPadding)
|
||||
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
|
||||
@@ -160,8 +160,7 @@ private fun FeedLoaded(
|
||||
) {
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.id() }) { _, item ->
|
||||
val defaultModifier = remember {
|
||||
Modifier
|
||||
.fillMaxWidth().animateItemPlacement()
|
||||
Modifier.fillMaxWidth().animateItemPlacement()
|
||||
}
|
||||
|
||||
Row(defaultModifier) {
|
||||
|
||||
Reference in New Issue
Block a user