- 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.combinedClickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription 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.LoadNote
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -45,7 +47,6 @@ fun GalleryCardCompose(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
ratio: Float = 1.0f,
) { ) {
WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, nav, shortPreview = true) { WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, nav, shortPreview = true) {
CheckHiddenFeedWatchBlockAndReport( CheckHiddenFeedWatchBlockAndReport(
@@ -66,15 +67,15 @@ fun GalleryCardCompose(
} }
if (redirectToEventId != null) { if (redirectToEventId != null) {
LoadNote(baseNoteHex = redirectToEventId, accountViewModel = accountViewModel) { sourceNote -> LoadNote(baseNoteHex = redirectToEventId, accountViewModel = accountViewModel) { baseSourceNote ->
if (sourceNote != null) { if (baseSourceNote != null) {
val sourceNote by observeNote(baseSourceNote, accountViewModel)
RedirectableGalleryCard( RedirectableGalleryCard(
galleryNote = baseNote, galleryNote = baseNote,
sourceNote = sourceNote, sourceNote = sourceNote.note,
modifier = modifier, modifier = modifier,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
ratio = ratio,
) )
} else { } else {
RedirectableGalleryCard( RedirectableGalleryCard(
@@ -83,7 +84,6 @@ fun GalleryCardCompose(
modifier = modifier, modifier = modifier,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
ratio = ratio,
) )
} }
} }
@@ -94,7 +94,6 @@ fun GalleryCardCompose(
modifier = modifier, modifier = modifier,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
ratio = ratio,
) )
} }
} }
@@ -108,7 +107,6 @@ fun RedirectableGalleryCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
ratio: Float = 1.0f,
) { ) {
QuickActionGallery(baseNote = galleryNote, accountViewModel = accountViewModel) { showPopup -> QuickActionGallery(baseNote = galleryNote, accountViewModel = accountViewModel) { showPopup ->
ClickableNote( ClickableNote(
@@ -127,7 +125,7 @@ fun RedirectableGalleryCard(
note = galleryNote, note = galleryNote,
accountViewModel = accountViewModel, 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 package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery
import android.content.Context
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size 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.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
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
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import coil3.compose.AsyncImagePainter import coil3.compose.AsyncImagePainter
import coil3.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImage
import coil3.compose.SubcomposeAsyncImageContent import coil3.compose.SubcomposeAsyncImageContent
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.isVideoUrl import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.isVideoUrl
import com.vitorpamplona.amethyst.model.Note 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.service.relayClient.reqCommand.event.observeNote
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid 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.DisplayBlurHash
import com.vitorpamplona.amethyst.ui.components.ImageUrlWithDownloadButton
import com.vitorpamplona.amethyst.ui.components.LoadingAnimation import com.vitorpamplona.amethyst.ui.components.LoadingAnimation
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.navigation.navs.INav 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.WatchAuthor
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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.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.experimental.profileGallery.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoEvent import com.vitorpamplona.quartz.nip71Video.VideoEvent
import kotlinx.coroutines.flow.map
@Composable @Composable
fun GalleryThumbnail( fun GalleryThumbnail(
baseNote: Note, baseNote: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
ratio: Float = 1.0f,
) { ) {
val noteState by observeNote(baseNote, accountViewModel) val noteState by observeNote(baseNote, accountViewModel)
val noteEvent = noteState.note.event ?: return val noteEvent = noteState.note.event ?: return
@@ -137,7 +132,7 @@ fun GalleryThumbnail(
emptyList() emptyList()
} }
InnerRenderGalleryThumb(content, baseNote, accountViewModel, ratio) InnerRenderGalleryThumb(content, baseNote, accountViewModel)
} }
@Composable @Composable
@@ -145,10 +140,9 @@ fun InnerRenderGalleryThumb(
content: List<MediaUrlContent>, content: List<MediaUrlContent>,
note: Note, note: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
ratio: Float = 1.0f,
) { ) {
if (content.isNotEmpty()) { if (content.isNotEmpty()) {
GalleryContentView(content, accountViewModel, ratio = ratio) GalleryContentView(content, accountViewModel)
} else { } else {
DisplayGalleryAuthorBanner(note, accountViewModel) DisplayGalleryAuthorBanner(note, accountViewModel)
} }
@@ -160,28 +154,30 @@ fun DisplayGalleryAuthorBanner(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
WatchAuthor(note, accountViewModel) { author -> 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 @Composable
fun GalleryContentView( fun GalleryContentView(
contentList: List<MediaUrlContent>, contentList: List<MediaUrlContent>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
ratio: Float = 1.0f,
) { ) {
AutoNonlazyGrid(contentList.size) { contentIndex -> AutoNonlazyGrid(contentList.size, modifier = Modifier.fillMaxSize()) { contentIndex ->
when (val content = contentList[contentIndex]) { when (val content = contentList[contentIndex]) {
is MediaUrlImage -> { is MediaUrlContent -> {
SensitivityWarning(content.contentWarning != null, accountViewModel) { val hasSensitiveContent =
UrlImageView(content, accountViewModel, ratio = ratio) (content is MediaUrlVideo && content.contentWarning != null) ||
} (content is MediaUrlImage && content.contentWarning != null)
} SensitivityWarning(hasSensitiveContent, accountViewModel) {
UrlImageView(content, accountViewModel)
is MediaUrlVideo -> {
SensitivityWarning(content.contentWarning != null, accountViewModel) {
UrlVideoView(content, accountViewModel, ratio = ratio)
} }
} }
} }
@@ -190,17 +186,15 @@ fun GalleryContentView(
@Composable @Composable
fun UrlImageView( fun UrlImageView(
content: MediaUrlImage, content: MediaUrlContent,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
alwayShowImage: Boolean = false,
ratio: Float = 1.0f,
) { ) {
val defaultModifier = Modifier.fillMaxSize().aspectRatio(ratio) val defaultModifier = Modifier.fillMaxSize()
val showImage = val showImage =
remember { remember {
mutableStateOf( mutableStateOf(
if (alwayShowImage) true else accountViewModel.settings.showImages(), accountViewModel.settings.showImages(),
) )
} }
@@ -231,7 +225,14 @@ fun UrlImageView(
} }
is AsyncImagePainter.State.Error -> { 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 -> { is AsyncImagePainter.State.Success -> {
@@ -249,86 +250,19 @@ fun UrlImageView(
ContentScale.Crop, ContentScale.Crop,
defaultModifier.clickable { showImage.value = true }, defaultModifier.clickable { showImage.value = true },
) )
IconButton( Icon(
modifier = Modifier.size(Size75dp), imageVector = Icons.Default.PlayCircleOutline,
onClick = { showImage.value = true }, contentDescription = stringRes(id = R.string.play),
) { modifier = Size50Modifier,
DownloadForOfflineIcon(Size75dp, Color.White) tint = Color.White,
} )
} else { } else {
ImageUrlWithDownloadButton(content.url, showImage) Icon(
} imageVector = Icons.Default.PlayCircleOutline,
} contentDescription = stringRes(id = R.string.play),
} modifier = Size50Modifier,
} tint = Color.White,
)
@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
}
},
)
}
} }
} }
} }
@@ -115,7 +115,6 @@ private fun GalleryFeedLoaded(
.animateItem(), .animateItem(),
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, 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-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-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 = { 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-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" } androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
androidx-security-crypto-ktx = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "securityCryptoKtx" } 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-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-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-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" } 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-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" }