- Migrates Gallery to not play videos and simply display a tumbnail when it is a video

- Fixes incorrect aspect ratios of the AutoNonLazyGrid when multiple images exist for a single entry
- Adds Play buttons when the image fails to load so that people click and the post appears.
This commit is contained in:
Vitor Pamplona
2026-02-23 19:28:53 -05:00
parent 6c12476902
commit 5e2a4b2cef
4 changed files with 57 additions and 124 deletions
@@ -24,10 +24,12 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -45,7 +47,6 @@ fun GalleryCardCompose(
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: INav,
ratio: Float = 1.0f,
) {
WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, nav, shortPreview = true) {
CheckHiddenFeedWatchBlockAndReport(
@@ -66,15 +67,15 @@ fun GalleryCardCompose(
}
if (redirectToEventId != null) {
LoadNote(baseNoteHex = redirectToEventId, accountViewModel = accountViewModel) { sourceNote ->
if (sourceNote != null) {
LoadNote(baseNoteHex = redirectToEventId, accountViewModel = accountViewModel) { baseSourceNote ->
if (baseSourceNote != null) {
val sourceNote by observeNote(baseSourceNote, accountViewModel)
RedirectableGalleryCard(
galleryNote = baseNote,
sourceNote = sourceNote,
sourceNote = sourceNote.note,
modifier = modifier,
accountViewModel = accountViewModel,
nav = nav,
ratio = ratio,
)
} else {
RedirectableGalleryCard(
@@ -83,7 +84,6 @@ fun GalleryCardCompose(
modifier = modifier,
accountViewModel = accountViewModel,
nav = nav,
ratio = ratio,
)
}
}
@@ -94,7 +94,6 @@ fun GalleryCardCompose(
modifier = modifier,
accountViewModel = accountViewModel,
nav = nav,
ratio = ratio,
)
}
}
@@ -108,7 +107,6 @@ fun RedirectableGalleryCard(
modifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: INav,
ratio: Float = 1.0f,
) {
QuickActionGallery(baseNote = galleryNote, accountViewModel = accountViewModel) { showPopup ->
ClickableNote(
@@ -127,7 +125,7 @@ fun RedirectableGalleryCard(
note = galleryNote,
accountViewModel = accountViewModel,
) {
GalleryThumbnail(galleryNote, accountViewModel, nav, ratio = ratio)
GalleryThumbnail(galleryNote, accountViewModel, nav)
}
}
}
@@ -20,63 +20,58 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery
import android.content.Context
import androidx.annotation.OptIn
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayCircleOutline
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import coil3.compose.AsyncImagePainter
import coil3.compose.SubcomposeAsyncImage
import coil3.compose.SubcomposeAsyncImageContent
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.isVideoUrl
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.components.DisplayBlurHash
import com.vitorpamplona.amethyst.ui.components.ImageUrlWithDownloadButton
import com.vitorpamplona.amethyst.ui.components.LoadingAnimation
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
import com.vitorpamplona.amethyst.ui.note.WatchAuthor
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size75dp
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoEvent
import kotlinx.coroutines.flow.map
@Composable
fun GalleryThumbnail(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
ratio: Float = 1.0f,
) {
val noteState by observeNote(baseNote, accountViewModel)
val noteEvent = noteState.note.event ?: return
@@ -137,7 +132,7 @@ fun GalleryThumbnail(
emptyList()
}
InnerRenderGalleryThumb(content, baseNote, accountViewModel, ratio)
InnerRenderGalleryThumb(content, baseNote, accountViewModel)
}
@Composable
@@ -145,10 +140,9 @@ fun InnerRenderGalleryThumb(
content: List<MediaUrlContent>,
note: Note,
accountViewModel: AccountViewModel,
ratio: Float = 1.0f,
) {
if (content.isNotEmpty()) {
GalleryContentView(content, accountViewModel, ratio = ratio)
GalleryContentView(content, accountViewModel)
} else {
DisplayGalleryAuthorBanner(note, accountViewModel)
}
@@ -160,28 +154,30 @@ fun DisplayGalleryAuthorBanner(
accountViewModel: AccountViewModel,
) {
WatchAuthor(note, accountViewModel) { author ->
BannerImage(author, Modifier.fillMaxSize().clip(QuoteBorder), accountViewModel)
BannerImage(
author,
Modifier
.fillMaxSize()
.clip(QuoteBorder),
accountViewModel,
)
}
}
@androidx.annotation.OptIn(UnstableApi::class)
@OptIn(UnstableApi::class)
@Composable
fun GalleryContentView(
contentList: List<MediaUrlContent>,
accountViewModel: AccountViewModel,
ratio: Float = 1.0f,
) {
AutoNonlazyGrid(contentList.size) { contentIndex ->
AutoNonlazyGrid(contentList.size, modifier = Modifier.fillMaxSize()) { contentIndex ->
when (val content = contentList[contentIndex]) {
is MediaUrlImage -> {
SensitivityWarning(content.contentWarning != null, accountViewModel) {
UrlImageView(content, accountViewModel, ratio = ratio)
}
}
is MediaUrlVideo -> {
SensitivityWarning(content.contentWarning != null, accountViewModel) {
UrlVideoView(content, accountViewModel, ratio = ratio)
is MediaUrlContent -> {
val hasSensitiveContent =
(content is MediaUrlVideo && content.contentWarning != null) ||
(content is MediaUrlImage && content.contentWarning != null)
SensitivityWarning(hasSensitiveContent, accountViewModel) {
UrlImageView(content, accountViewModel)
}
}
}
@@ -190,17 +186,15 @@ fun GalleryContentView(
@Composable
fun UrlImageView(
content: MediaUrlImage,
content: MediaUrlContent,
accountViewModel: AccountViewModel,
alwayShowImage: Boolean = false,
ratio: Float = 1.0f,
) {
val defaultModifier = Modifier.fillMaxSize().aspectRatio(ratio)
val defaultModifier = Modifier.fillMaxSize()
val showImage =
remember {
mutableStateOf(
if (alwayShowImage) true else accountViewModel.settings.showImages(),
accountViewModel.settings.showImages(),
)
}
@@ -231,7 +225,14 @@ fun UrlImageView(
}
is AsyncImagePainter.State.Error -> {
ClickableUrl(urlText = "${content.url} ", url = content.url)
Box(defaultModifier, contentAlignment = Alignment.Center) {
Icon(
imageVector = Icons.Default.PlayCircleOutline,
contentDescription = stringRes(id = R.string.play),
modifier = Size50Modifier,
tint = Color.White,
)
}
}
is AsyncImagePainter.State.Success -> {
@@ -249,86 +250,19 @@ fun UrlImageView(
ContentScale.Crop,
defaultModifier.clickable { showImage.value = true },
)
IconButton(
modifier = Modifier.size(Size75dp),
onClick = { showImage.value = true },
) {
DownloadForOfflineIcon(Size75dp, Color.White)
}
Icon(
imageVector = Icons.Default.PlayCircleOutline,
contentDescription = stringRes(id = R.string.play),
modifier = Size50Modifier,
tint = Color.White,
)
} else {
ImageUrlWithDownloadButton(content.url, showImage)
}
}
}
}
@OptIn(UnstableApi::class)
@Composable
fun UrlVideoView(
content: MediaUrlVideo,
accountViewModel: AccountViewModel,
ratio: Float = 1.0f,
) {
val defaultModifier = Modifier.fillMaxSize().aspectRatio(ratio)
val automaticallyStartPlayback =
remember(content) {
mutableStateOf<Boolean>(accountViewModel.settings.startVideoPlayback())
}
Box(defaultModifier, contentAlignment = Alignment.Center) {
if (content.blurhash != null) {
// Always displays Blurharh to avoid size flickering
DisplayBlurHash(
content.blurhash,
null,
ContentScale.Crop,
defaultModifier,
)
}
if (!automaticallyStartPlayback.value) {
IconButton(
modifier = Modifier.size(Size75dp),
onClick = { automaticallyStartPlayback.value = true },
) {
DownloadForOfflineIcon(Size75dp, Color.White)
}
} else {
GetMediaItem(
videoUri = content.url,
title = content.description,
artworkUri = content.artworkUri,
authorName = content.authorName,
callbackUri = content.uri,
mimeType = content.mimeType,
aspectRatio = ratio,
proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(content.url),
) { mediaItem ->
GetVideoController(
mediaItem = mediaItem,
muted = true,
) { controller ->
AndroidView(
modifier = Modifier,
factory = { context: Context ->
PlayerView(context).apply {
clipToOutline = true
player = controller.controller
setShowBuffering(PlayerView.SHOW_BUFFERING_ALWAYS)
controllerAutoShow = false
useController = false
hideController()
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL
controller.controller?.playWhenReady = true
}
},
)
}
Icon(
imageVector = Icons.Default.PlayCircleOutline,
contentDescription = stringRes(id = R.string.play),
modifier = Size50Modifier,
tint = Color.White,
)
}
}
}
@@ -115,7 +115,6 @@ private fun GalleryFeedLoaded(
.animateItem(),
accountViewModel = accountViewModel,
nav = nav,
ratio = ratio,
)
}
}
+2
View File
@@ -100,6 +100,7 @@ androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplaye
androidx-media3-exoplayer-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "media3" }
androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }
androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
androidx-media3-ui-compose-material3 = { group = "androidx.media3", name = "media3-ui-compose-material3", version.ref = "media3" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
androidx-security-crypto-ktx = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "securityCryptoKtx" }
@@ -114,6 +115,7 @@ coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref
coil-gif = { group = "io.coil-kt.coil3", name = "coil-gif", version.ref = "coil" }
coil-svg = { group = "io.coil-kt.coil3", name = "coil-svg", version.ref = "coil" }
coil-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }
coil-video = { group = "io.coil-kt.coil3", name = "coil-video", version.ref = "coil" }
drfonfon-geohash = { group = "com.github.drfonfon", name = "android-kotlin-geohash", version.ref = "androidKotlinGeohash" }
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" }