Merge pull request #2817 from davotoula/fix/gallery-forward-thumbhash

fix(gallery): forward imeta hashes through gallery
This commit is contained in:
Vitor Pamplona
2026-05-09 12:42:57 -04:00
committed by GitHub
8 changed files with 65 additions and 9 deletions
@@ -70,6 +70,7 @@ fun VideoView(
alwaysShowVideo: Boolean = false,
thumbhash: String? = null,
isLiveStream: Boolean = false,
hash: String? = null,
) {
val borderModifier =
if (roundedCorner) {
@@ -80,7 +81,7 @@ fun VideoView(
Modifier
}
VideoView(videoUri, mimeType, title, thumb, borderModifier, contentScale, waveform, artworkUri, authorName, dimensions, blurhash, nostrUriCallback, onDialog, alwaysShowVideo, accountViewModel = accountViewModel, thumbhash = thumbhash, isLiveStream = isLiveStream)
VideoView(videoUri, mimeType, title, thumb, borderModifier, contentScale, waveform, artworkUri, authorName, dimensions, blurhash, nostrUriCallback, onDialog, alwaysShowVideo, accountViewModel = accountViewModel, thumbhash = thumbhash, isLiveStream = isLiveStream, hash = hash)
}
@Composable
@@ -103,6 +104,7 @@ fun VideoView(
isLiveStream: Boolean = false,
accountViewModel: AccountViewModel,
thumbhash: String? = null,
hash: String? = null,
) {
val initialAutoStart = if (alwaysShowVideo) true else accountViewModel.settings.startVideoPlayback()
// Reset the manual-show toggle when the video URI changes so a recycled feed slot
@@ -160,6 +162,10 @@ fun VideoView(
isLiveStream = isLiveStream,
onZoom = onDialog,
hasBlurhash = false,
blurhash = blurhash,
dim = dimensions,
hash = hash,
thumbhash = thumbhash,
accountViewModel = accountViewModel,
showControls = showControls,
)
@@ -207,6 +213,10 @@ fun VideoView(
isLiveStream = isLiveStream,
onZoom = onDialog,
hasBlurhash = true,
blurhash = blurhash,
dim = dimensions,
hash = hash,
thumbhash = thumbhash,
accountViewModel = accountViewModel,
showControls = showControls,
)
@@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.service.playback.composable.controls.VideoQual
import com.vitorpamplona.amethyst.service.playback.composable.mainVideo.VideoPlayerActiveMutex
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
val DEFAULT_MUTED_SETTING = mutableStateOf(true)
@@ -54,6 +55,10 @@ fun VideoViewInner(
onZoom: (() -> Unit)? = null,
hasBlurhash: Boolean = false,
isFullscreen: Boolean = false,
blurhash: String? = null,
dim: DimensionTag? = null,
hash: String? = null,
thumbhash: String? = null,
accountViewModel: AccountViewModel,
) {
// keeps a copy of the value to avoid recompositions here when the DEFAULT value changes
@@ -76,6 +81,10 @@ fun VideoViewInner(
keepPlaying = true,
waveformData = waveform,
isLiveStream = isLiveStream,
blurhash = blurhash,
dim = dim,
hash = hash,
thumbhash = thumbhash,
) { mediaItem ->
GetVideoController(
mediaItem = mediaItem,
@@ -303,9 +303,9 @@ fun RenderTopButtons(
popupExpanded = shareDialogVisible,
videoUri = mediaData.videoUri,
postNostrUri = mediaData.callbackUri,
blurhash = null,
dim = null,
hash = null,
blurhash = mediaData.blurhash,
dim = mediaData.dim,
hash = mediaData.hash,
mimeType = mediaData.mimeType,
onDismiss = { shareDialogVisible.value = false },
content =
@@ -316,6 +316,10 @@ fun RenderTopButtons(
authorName = mediaData.authorName,
description = mediaData.title,
uri = mediaData.callbackUri,
blurhash = mediaData.blurhash,
dim = mediaData.dim,
hash = mediaData.hash,
thumbhash = mediaData.thumbhash,
),
accountViewModel = accountViewModel,
)
@@ -25,6 +25,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.compose.produceCachedState
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
val mediaItemCache = MediaItemCache()
@@ -41,6 +42,10 @@ fun GetMediaItem(
keepPlaying: Boolean = false,
waveformData: WaveformData? = null,
isLiveStream: Boolean = false,
blurhash: String? = null,
dim: DimensionTag? = null,
hash: String? = null,
thumbhash: String? = null,
inner: @Composable (LoadedMediaItem) -> Unit,
) {
val data =
@@ -57,6 +62,10 @@ fun GetMediaItem(
keepPlaying = keepPlaying,
waveformData = waveformData,
isLiveStream = isLiveStream,
blurhash = blurhash,
dim = dim,
hash = hash,
thumbhash = thumbhash,
)
}
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.playback.composable.mediaitem
import androidx.compose.runtime.Immutable
import androidx.media3.common.MediaItem
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
@Immutable
data class MediaItemData(
@@ -40,6 +41,13 @@ data class MediaItemData(
// (e.g. multi-rendition NIP-71 videos) must leave this false so that
// ExoPlayer's SimpleCache can cache their immutable segments.
val isLiveStream: Boolean = false,
// Source-imeta visual fields — carried through so the player's "Add to Gallery" /
// "Share" actions can publish a richer ProfileGalleryEntryEvent. Optional; null when
// the surface that constructed this MediaItemData didn't have them.
val blurhash: String? = null,
val dim: DimensionTag? = null,
val hash: String? = null,
val thumbhash: String? = null,
)
@Immutable
@@ -48,6 +48,8 @@ data class PreviewHashes(
}
object PreviewMetadataCalculator {
private const val LOG_TAG = "PreviewMetadataCalc"
private fun isImage(mimeType: String?) = mimeType?.startsWith("image/", ignoreCase = true) == true
private fun isVideo(mimeType: String?) = mimeType?.startsWith("video/", ignoreCase = true) == true
@@ -117,7 +119,7 @@ object PreviewMetadataCalculator {
}
}
} catch (e: Exception) {
Log.w("PreviewMetadataCalc", "Failed to compute metadata from uri", e)
Log.w(LOG_TAG, "Failed to compute metadata from uri", e)
null
}
}
@@ -133,8 +135,14 @@ object PreviewMetadataCalculator {
private fun processBitmap(bitmap: Bitmap?): PreviewHashes =
if (bitmap != null) {
try {
val blurhash = runCatching { BlurhashWrapper(bitmap.toBlurhash()) }.getOrNull()
val thumbhash = runCatching { ThumbhashWrapper(bitmap.toThumbhash()) }.getOrNull()
val blurhash =
runCatching { BlurhashWrapper(bitmap.toBlurhash()) }
.onFailure { Log.w(LOG_TAG, "blurhash generation failed", it) }
.getOrNull()
val thumbhash =
runCatching { ThumbhashWrapper(bitmap.toThumbhash()) }
.onFailure { Log.w(LOG_TAG, "thumbhash generation failed", it) }
.getOrNull()
PreviewHashes(
blurhash = blurhash,
thumbhash = thumbhash,
@@ -153,6 +161,9 @@ object PreviewMetadataCalculator {
): PreviewHashes {
val dim = retriever.prepareDimFromVideo() ?: dimPrecomputed
val thumb = retriever.getThumbnail()
if (thumb == null) {
Log.w(LOG_TAG) { "video frame extraction returned null; no blurhash/thumbhash will be generated" }
}
val hashes = processBitmap(thumb)
val finalDim = if (dim?.hasSize() == true) dim else hashes.dim
return hashes.copy(dim = finalDim)
@@ -226,6 +226,7 @@ fun ZoomableContentView(
accountViewModel = accountViewModel,
thumbhash = content.thumbhash,
isLiveStream = content.isLiveStream,
hash = content.hash,
)
}
}
@@ -961,7 +962,10 @@ fun ShareMediaAction(
// still — the .m3u8 URL itself is a text manifest. Only
// MediaUrlVideo carries an artworkUri.
val posterUrl = (content as? MediaUrlVideo)?.artworkUri
accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay.getOrNull(0), blurhash, dim, hash, mimeType, image = posterUrl)
val thumbhashFromContent =
(content as? MediaUrlVideo)?.thumbhash
?: (content as? MediaUrlImage)?.thumbhash
accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay.getOrNull(0), blurhash, dim, hash, mimeType, thumbhash = thumbhashFromContent, image = posterUrl)
accountViewModel.toastManager.toast(R.string.media_added, R.string.media_added_to_profile_gallery)
}
}
@@ -896,8 +896,9 @@ class AccountViewModel(
dim: DimensionTag?,
hash: String?,
mimeType: String?,
thumbhash: String? = null,
image: String? = null,
) = launchSigner { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType, image = image) }
) = launchSigner { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType, thumbhash = thumbhash, image = image) }
fun removeFromMediaGallery(note: Note) = launchSigner { account.removeFromGallery(note) }