mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-26 17:52:29 +01:00
Move gallery previews to it's own package. Avoid reusing VideoView
This commit is contained in:
parent
01b880c4a5
commit
e65346d268
@ -27,7 +27,6 @@ import androidx.compose.animation.fadeOut
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
@ -95,7 +94,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
@ -135,7 +133,10 @@ fun ZoomableContentView(
|
||||
is MediaUrlImage ->
|
||||
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
||||
TwoSecondController(content) { controllerVisible ->
|
||||
val mainImageModifier = Modifier.fillMaxWidth().clickable { dialogOpen = true }
|
||||
val mainImageModifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { dialogOpen = true }
|
||||
val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
|
||||
|
||||
UrlImageView(content, contentScale, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
||||
@ -167,7 +168,10 @@ fun ZoomableContentView(
|
||||
}
|
||||
is MediaLocalImage ->
|
||||
TwoSecondController(content) { controllerVisible ->
|
||||
val mainImageModifier = Modifier.fillMaxWidth().clickable { dialogOpen = true }
|
||||
val mainImageModifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { dialogOpen = true }
|
||||
val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
|
||||
|
||||
LocalImageView(content, contentScale, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
||||
@ -204,61 +208,6 @@ fun ZoomableContentView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GalleryContentView(
|
||||
content: BaseMediaContent,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
when (content) {
|
||||
is MediaUrlImage ->
|
||||
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
||||
TwoSecondController(content) { controllerVisible ->
|
||||
UrlImageView(content, ContentScale.Crop, Modifier.fillMaxSize(), Modifier.fillMaxSize(), controllerVisible, accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
is MediaUrlVideo ->
|
||||
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
VideoView(
|
||||
videoUri = content.url,
|
||||
mimeType = content.mimeType,
|
||||
title = content.description,
|
||||
artworkUri = content.artworkUri,
|
||||
borderModifier = MaterialTheme.colorScheme.videoGalleryModifier,
|
||||
authorName = content.authorName,
|
||||
dimensions = Dimension(1, 1), // fit video in 1:1 ratio
|
||||
blurhash = content.blurhash,
|
||||
isFiniteHeight = false,
|
||||
nostrUriCallback = content.uri,
|
||||
accountViewModel = accountViewModel,
|
||||
alwaysShowVideo = true,
|
||||
showControls = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
is MediaLocalImage ->
|
||||
TwoSecondController(content) { controllerVisible ->
|
||||
LocalImageView(content, ContentScale.Crop, Modifier.fillMaxSize(), Modifier.fillMaxSize(), controllerVisible, accountViewModel = accountViewModel)
|
||||
}
|
||||
is MediaLocalVideo ->
|
||||
content.localFile?.let {
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
VideoView(
|
||||
videoUri = it.toUri().toString(),
|
||||
mimeType = content.mimeType,
|
||||
title = content.description,
|
||||
artworkUri = content.artworkUri,
|
||||
authorName = content.authorName,
|
||||
borderModifier = MaterialTheme.colorScheme.videoGalleryModifier,
|
||||
isFiniteHeight = false,
|
||||
nostrUriCallback = content.uri,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TwoSecondController(
|
||||
content: BaseMediaContent,
|
||||
@ -501,7 +450,12 @@ fun ImageUrlWithDownloadButton(
|
||||
|
||||
val inlineContent = mapOf("inlineContent" to InlineDownloadIcon(showImage))
|
||||
|
||||
val pressIndicator = remember { Modifier.fillMaxWidth().clickable { runCatching { uri.openUri(url) } } }
|
||||
val pressIndicator =
|
||||
remember {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { runCatching { uri.openUri(url) } }
|
||||
}
|
||||
|
||||
Text(
|
||||
text = annotatedTermsString,
|
||||
@ -555,7 +509,7 @@ fun aspectRatio(dim: Dimension?): Float? {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayUrlWithLoadingSymbol(content: BaseMediaContent) {
|
||||
fun DisplayUrlWithLoadingSymbol(content: BaseMediaContent) {
|
||||
val uri = LocalUriHandler.current
|
||||
|
||||
val primary = MaterialTheme.colorScheme.primary
|
||||
|
@ -160,40 +160,6 @@ fun LongPressToQuickAction(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LongPressToQuickActionGallery(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (() -> Unit) -> Unit,
|
||||
) {
|
||||
val popupExpanded = remember { mutableStateOf(false) }
|
||||
|
||||
content { popupExpanded.value = true }
|
||||
|
||||
if (popupExpanded.value) {
|
||||
if (baseNote.author == accountViewModel.account.userProfile()) {
|
||||
NoteQuickActionMenuGallery(
|
||||
note = baseNote,
|
||||
onDismiss = { popupExpanded.value = false },
|
||||
accountViewModel = accountViewModel,
|
||||
nav = EmptyNav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteQuickActionMenuGallery(
|
||||
note: Note,
|
||||
onDismiss: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
DeleteFromGalleryDialog(note, accountViewModel) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteQuickActionMenu(
|
||||
note: Note,
|
||||
@ -657,25 +623,6 @@ fun NoteQuickActionItem(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteFromGalleryDialog(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
QuickActionAlertDialogOneButton(
|
||||
title = stringRes(R.string.quick_action_request_deletion_gallery_title),
|
||||
textContent = stringRes(R.string.quick_action_request_deletion_gallery_alert_body),
|
||||
buttonIcon = Icons.Default.Delete,
|
||||
buttonText = stringRes(R.string.quick_action_delete_dialog_btn),
|
||||
onClickDoOnce = {
|
||||
accountViewModel.removefromMediaGallery(note)
|
||||
onDismiss()
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteAlertDialog(
|
||||
note: Note,
|
||||
|
@ -1,409 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment.Companion.BottomStart
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
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.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.GalleryContentView
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LongPressToQuickActionGallery
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchAuthor
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.quartz.events.PictureEvent
|
||||
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.events.VideoEvent
|
||||
|
||||
@Composable
|
||||
fun RenderGalleryFeed(
|
||||
viewModel: FeedViewModel,
|
||||
routeForLastRead: String?,
|
||||
listState: LazyGridState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val feedState by viewModel.feedState.feedContent.collectAsStateWithLifecycle()
|
||||
CrossfadeIfEnabled(
|
||||
targetState = feedState,
|
||||
animationSpec = tween(durationMillis = 100),
|
||||
label = "RenderDiscoverFeed",
|
||||
accountViewModel = accountViewModel,
|
||||
) { state ->
|
||||
when (state) {
|
||||
is FeedState.Empty -> {
|
||||
FeedEmpty { viewModel.invalidateData() }
|
||||
}
|
||||
is FeedState.FeedError -> {
|
||||
FeedError(state.errorMessage) { viewModel.invalidateData() }
|
||||
}
|
||||
is FeedState.Loaded -> {
|
||||
GalleryFeedLoaded(
|
||||
state,
|
||||
routeForLastRead,
|
||||
listState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
is FeedState.Loading -> {
|
||||
LoadingFeed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun GalleryFeedLoaded(
|
||||
loaded: FeedState.Loaded,
|
||||
routeForLastRead: String?,
|
||||
listState: LazyGridState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
contentPadding = FeedPadding,
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
|
||||
GalleryCardCompose(
|
||||
baseNote = item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = Modifier.fillMaxSize().animateItem().padding(Size5dp),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GalleryCardCompose(
|
||||
baseNote: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
isHiddenFeed: Boolean = false,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, shortPreview = true) {
|
||||
CheckHiddenFeedWatchBlockAndReport(
|
||||
note = baseNote,
|
||||
modifier = modifier,
|
||||
ignoreAllBlocksAndReports = isHiddenFeed,
|
||||
showHiddenWarning = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
) { canPreview ->
|
||||
val (url, sourceEvent) =
|
||||
when (val galleryEvent = baseNote.event) {
|
||||
is ProfileGalleryEntryEvent -> Pair(galleryEvent.url(), galleryEvent.fromEvent())
|
||||
is PictureEvent -> Pair(galleryEvent.imetaTags().getOrNull(0)?.url, galleryEvent.id())
|
||||
is VideoEvent -> Pair(galleryEvent.imetaTags().getOrNull(0)?.url, galleryEvent.id())
|
||||
else -> Pair(null, null)
|
||||
}
|
||||
|
||||
url?.let { imageUrl ->
|
||||
if (sourceEvent != null) {
|
||||
LoadNote(baseNoteHex = sourceEvent, accountViewModel = accountViewModel) { sourceNote ->
|
||||
if (sourceNote != null) {
|
||||
ClickableGalleryCard(
|
||||
galleryNote = baseNote,
|
||||
baseNote = sourceNote,
|
||||
image = imageUrl,
|
||||
modifier = modifier,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
} else {
|
||||
GalleryCard(
|
||||
galleryNote = baseNote,
|
||||
image = imageUrl,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
GalleryCard(
|
||||
galleryNote = baseNote,
|
||||
image = imageUrl,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ClickableGalleryCard(
|
||||
galleryNote: Note,
|
||||
baseNote: Note,
|
||||
image: String,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
// baseNote.event?.let { Text(text = it.pubKey()) }
|
||||
LongPressToQuickActionGallery(baseNote = galleryNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
val backgroundColor =
|
||||
calculateBackgroundColor(
|
||||
createdAt = baseNote.createdAt(),
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
|
||||
ClickableNote(
|
||||
baseNote = baseNote,
|
||||
backgroundColor = backgroundColor,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
showPopup = showPopup,
|
||||
nav = nav,
|
||||
) {
|
||||
InnerGalleryCardBox(galleryNote, image, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GalleryCard(
|
||||
galleryNote: Note,
|
||||
image: String,
|
||||
modifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
LongPressToQuickActionGallery(baseNote = galleryNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
Column(modifier = modifier) {
|
||||
InnerGalleryCardBox(galleryNote, image, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InnerGalleryCardBox(
|
||||
baseNote: Note,
|
||||
image: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
SensitivityWarning(
|
||||
note = baseNote,
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
RenderGalleryThumb(baseNote, image, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class GalleryThumb(
|
||||
val id: String?,
|
||||
val image: String?,
|
||||
val title: String?,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun RenderGalleryThumb(
|
||||
baseNote: Note,
|
||||
image: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val card by
|
||||
baseNote
|
||||
.live()
|
||||
.metadata
|
||||
.map {
|
||||
GalleryThumb(
|
||||
id = "",
|
||||
image = image,
|
||||
title = "",
|
||||
// noteEvent?.title(),
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
.observeAsState(
|
||||
GalleryThumb(
|
||||
id = "",
|
||||
image = image,
|
||||
title = "",
|
||||
),
|
||||
)
|
||||
|
||||
InnerRenderGalleryThumb(card, baseNote, accountViewModel)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RenderGalleryThumbPreview() {
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
|
||||
Surface(Modifier.size(200.dp)) {
|
||||
InnerRenderGalleryThumb(
|
||||
card =
|
||||
GalleryThumb(
|
||||
id = "",
|
||||
image = null,
|
||||
title = "Like New",
|
||||
),
|
||||
note = Note("hex"),
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InnerRenderGalleryThumb(
|
||||
card: GalleryThumb,
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val noteEvent = note.event
|
||||
val content =
|
||||
if (card.image == null) {
|
||||
null
|
||||
} else if (noteEvent is ProfileGalleryEntryEvent) {
|
||||
if (isVideoUrl(card.image)) {
|
||||
MediaUrlVideo(
|
||||
url = card.image,
|
||||
description = noteEvent.content,
|
||||
hash = null,
|
||||
blurhash = noteEvent.blurhash(),
|
||||
dim = noteEvent.dimensions(),
|
||||
uri = null,
|
||||
mimeType = noteEvent.mimeType(),
|
||||
)
|
||||
} else {
|
||||
MediaUrlImage(
|
||||
url = card.image,
|
||||
description = noteEvent.content,
|
||||
hash = null, // We don't want to show the hash banner here
|
||||
blurhash = noteEvent.blurhash(),
|
||||
dim = noteEvent.dimensions(),
|
||||
uri = null,
|
||||
mimeType = noteEvent.mimeType(),
|
||||
)
|
||||
}
|
||||
} else if (noteEvent is PictureEvent) {
|
||||
val imeta = noteEvent.imetaTags().firstOrNull()
|
||||
MediaUrlImage(
|
||||
url = card.image,
|
||||
description = noteEvent.content,
|
||||
hash = null, // We don't want to show the hash banner here
|
||||
blurhash = imeta?.blurhash,
|
||||
dim = imeta?.dimension,
|
||||
uri = null,
|
||||
mimeType = imeta?.mimeType,
|
||||
)
|
||||
} else if (noteEvent is VideoEvent) {
|
||||
val imeta = noteEvent.imetaTags().firstOrNull()
|
||||
MediaUrlVideo(
|
||||
url = card.image,
|
||||
description = noteEvent.content,
|
||||
hash = null, // We don't want to show the hash banner here
|
||||
blurhash = imeta?.blurhash,
|
||||
dim = imeta?.dimension,
|
||||
uri = null,
|
||||
mimeType = imeta?.mimeType,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
Box(Modifier.fillMaxWidth().aspectRatio(1f), BottomStart) {
|
||||
if (content != null) {
|
||||
GalleryContentView(
|
||||
content = content,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
} else {
|
||||
DisplayGalleryAuthorBanner(note)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayGalleryAuthorBanner(note: Note) {
|
||||
WatchAuthor(note) {
|
||||
BannerImage(
|
||||
it,
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.clip(QuoteBorder),
|
||||
)
|
||||
}
|
||||
}
|
@ -160,6 +160,7 @@ import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.showAmountAxis
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.RenderGalleryFeed
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRDialog
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
@ -1603,7 +1604,6 @@ fun TabGallery(
|
||||
SaveableGridFeedState(feedViewModel, scrollStateKey = ScrollStateKeys.PROFILE_GALLERY) { listState ->
|
||||
RenderGalleryFeed(
|
||||
feedViewModel,
|
||||
null,
|
||||
listState,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@ -1612,50 +1612,6 @@ fun TabGallery(
|
||||
}
|
||||
}
|
||||
|
||||
/*@Composable
|
||||
fun Gallery(
|
||||
baseUser: User,
|
||||
feedViewModel: UserFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchFollowChanges(baseUser, feedViewModel)
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column {
|
||||
baseUser.latestGalleryList?.let {
|
||||
// val note2 = getOrCreateAddressableNoteInternal(aTag)
|
||||
val note = LocalCache.getOrCreateAddressableNote(it.address())
|
||||
note.event = it
|
||||
var notes = listOf<GalleryThumb>()
|
||||
for (tag in note.event?.tags()!!) {
|
||||
if (tag.size > 2) {
|
||||
if (tag[0] == "g") {
|
||||
// TODO get the node by id on main thread. LoadNote does nothing.
|
||||
val thumb =
|
||||
GalleryThumb(
|
||||
baseNote = note,
|
||||
id = tag[2],
|
||||
// TODO use the original note once it's loaded baseNote = basenote,
|
||||
image = tag[1],
|
||||
title = null,
|
||||
)
|
||||
notes = notes + thumb
|
||||
// }
|
||||
}
|
||||
}
|
||||
ProfileGallery(
|
||||
baseNotes = notes,
|
||||
modifier = Modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
@Composable
|
||||
fun TabFollowedTags(
|
||||
baseUser: User,
|
||||
|
@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery
|
||||
|
||||
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.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
|
||||
@Composable
|
||||
fun GalleryCardCompose(
|
||||
baseNote: Note,
|
||||
modifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, shortPreview = true) {
|
||||
CheckHiddenFeedWatchBlockAndReport(
|
||||
note = baseNote,
|
||||
modifier = modifier,
|
||||
ignoreAllBlocksAndReports = false,
|
||||
showHiddenWarning = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
) { canPreview ->
|
||||
val galleryEvent = baseNote.event
|
||||
|
||||
val redirectToEventId =
|
||||
if (galleryEvent is ProfileGalleryEntryEvent) {
|
||||
galleryEvent.fromEvent()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (redirectToEventId != null) {
|
||||
LoadNote(baseNoteHex = redirectToEventId, accountViewModel = accountViewModel) { sourceNote ->
|
||||
if (sourceNote != null) {
|
||||
RedirectableGalleryCard(
|
||||
galleryNote = baseNote,
|
||||
sourceNote = sourceNote,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
} else {
|
||||
RedirectableGalleryCard(
|
||||
galleryNote = baseNote,
|
||||
sourceNote = baseNote,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
RedirectableGalleryCard(
|
||||
galleryNote = baseNote,
|
||||
sourceNote = baseNote,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RedirectableGalleryCard(
|
||||
galleryNote: Note,
|
||||
sourceNote: Note,
|
||||
modifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
QuickActionGallery(baseNote = galleryNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
ClickableNote(
|
||||
baseNote = sourceNote,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
showPopup = showPopup,
|
||||
nav = nav,
|
||||
) {
|
||||
if (sourceNote != galleryNote) {
|
||||
// preloads target note
|
||||
val loadedSourceEvent by sourceNote.live().hasEvent.observeAsState(sourceNote.event != null)
|
||||
}
|
||||
|
||||
SensitivityWarning(
|
||||
note = galleryNote,
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
GalleryThumbnail(galleryNote, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
fun ClickableNote(
|
||||
baseNote: Note,
|
||||
modifier: Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
showPopup: () -> Unit,
|
||||
nav: INav,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val updatedModifier =
|
||||
remember(baseNote, modifier) {
|
||||
modifier
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
val redirectToNote =
|
||||
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
|
||||
baseNote.replyTo?.lastOrNull() ?: baseNote
|
||||
} else {
|
||||
baseNote
|
||||
}
|
||||
routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav.nav(it) }
|
||||
},
|
||||
onLongClick = showPopup,
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = updatedModifier) { content() }
|
||||
}
|
@ -0,0 +1,320 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.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.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayBlurHash
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayUrlWithLoadingSymbol
|
||||
import com.vitorpamplona.amethyst.ui.components.GetMediaItem
|
||||
import com.vitorpamplona.amethyst.ui.components.GetVideoController
|
||||
import com.vitorpamplona.amethyst.ui.components.ImageUrlWithDownloadButton
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.UrlImageView
|
||||
import com.vitorpamplona.amethyst.ui.navigation.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.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import com.vitorpamplona.quartz.events.PictureEvent
|
||||
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.events.VideoEvent
|
||||
|
||||
@Composable
|
||||
fun GalleryThumbnail(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val noteEvent = noteState?.note?.event ?: return
|
||||
|
||||
val content =
|
||||
if (noteEvent is ProfileGalleryEntryEvent) {
|
||||
val url = noteEvent.url()
|
||||
if (url == null) {
|
||||
null
|
||||
} else if (isVideoUrl(url)) {
|
||||
MediaUrlVideo(
|
||||
url = url,
|
||||
description = noteEvent.content,
|
||||
hash = null,
|
||||
blurhash = noteEvent.blurhash(),
|
||||
dim = noteEvent.dimensions(),
|
||||
uri = null,
|
||||
mimeType = noteEvent.mimeType(),
|
||||
)
|
||||
} else {
|
||||
MediaUrlImage(
|
||||
url = url,
|
||||
description = noteEvent.content,
|
||||
hash = null, // We don't want to show the hash banner here
|
||||
blurhash = noteEvent.blurhash(),
|
||||
dim = noteEvent.dimensions(),
|
||||
uri = null,
|
||||
mimeType = noteEvent.mimeType(),
|
||||
)
|
||||
}
|
||||
} else if (noteEvent is PictureEvent) {
|
||||
val imeta = noteEvent.imetaTags().firstOrNull()
|
||||
if (imeta?.url == null) {
|
||||
null
|
||||
} else {
|
||||
MediaUrlImage(
|
||||
url = imeta.url,
|
||||
description = noteEvent.content,
|
||||
hash = null, // We don't want to show the hash banner here
|
||||
blurhash = imeta.blurhash,
|
||||
dim = imeta.dimension,
|
||||
uri = null,
|
||||
mimeType = imeta.mimeType,
|
||||
)
|
||||
}
|
||||
} else if (noteEvent is VideoEvent) {
|
||||
val imeta = noteEvent.imetaTags().firstOrNull()
|
||||
|
||||
if (imeta?.url == null) {
|
||||
null
|
||||
} else {
|
||||
MediaUrlVideo(
|
||||
url = imeta.url,
|
||||
description = noteEvent.content,
|
||||
hash = null, // We don't want to show the hash banner here
|
||||
blurhash = imeta.blurhash,
|
||||
dim = imeta.dimension,
|
||||
uri = null,
|
||||
mimeType = imeta.mimeType,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
InnerRenderGalleryThumb(content, baseNote, accountViewModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InnerRenderGalleryThumb(
|
||||
content: MediaUrlContent?,
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
if (content != null) {
|
||||
GalleryContentView(
|
||||
content = content,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
} else {
|
||||
DisplayGalleryAuthorBanner(note)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayGalleryAuthorBanner(note: Note) {
|
||||
WatchAuthor(note) { author ->
|
||||
BannerImage(author, Modifier.fillMaxSize().clip(QuoteBorder))
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
fun GalleryContentView(
|
||||
content: MediaUrlContent,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
when (content) {
|
||||
is MediaUrlImage ->
|
||||
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
||||
UrlImageView(content, accountViewModel)
|
||||
}
|
||||
is MediaUrlVideo ->
|
||||
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
||||
UrlVideoView(content, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UrlImageView(
|
||||
content: MediaUrlImage,
|
||||
accountViewModel: AccountViewModel,
|
||||
alwayShowImage: Boolean = false,
|
||||
) {
|
||||
val defaultModifier = Modifier.fillMaxSize().aspectRatio(1f)
|
||||
|
||||
val showImage =
|
||||
remember {
|
||||
mutableStateOf(
|
||||
if (alwayShowImage) true else accountViewModel.settings.showImages.value,
|
||||
)
|
||||
}
|
||||
|
||||
CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center, accountViewModel = accountViewModel) {
|
||||
if (it) {
|
||||
SubcomposeAsyncImage(
|
||||
model = content.url,
|
||||
contentDescription = content.description,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
val state by painter.state.collectAsState()
|
||||
when (state) {
|
||||
is AsyncImagePainter.State.Loading,
|
||||
-> {
|
||||
if (content.blurhash != null) {
|
||||
DisplayBlurHash(
|
||||
content.blurhash,
|
||||
content.description,
|
||||
ContentScale.Crop,
|
||||
defaultModifier,
|
||||
)
|
||||
} else {
|
||||
DisplayUrlWithLoadingSymbol(content)
|
||||
}
|
||||
}
|
||||
is AsyncImagePainter.State.Error -> {
|
||||
ClickableUrl(urlText = "${content.url} ", url = content.url)
|
||||
}
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
SubcomposeAsyncImageContent(defaultModifier)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (content.blurhash != null) {
|
||||
DisplayBlurHash(
|
||||
content.blurhash,
|
||||
content.description,
|
||||
ContentScale.Crop,
|
||||
defaultModifier.clickable { showImage.value = true },
|
||||
)
|
||||
IconButton(
|
||||
modifier = Modifier.size(Size75dp),
|
||||
onClick = { showImage.value = true },
|
||||
) {
|
||||
DownloadForOfflineIcon(Size75dp, Color.White)
|
||||
}
|
||||
} else {
|
||||
ImageUrlWithDownloadButton(content.url, showImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
fun UrlVideoView(
|
||||
content: MediaUrlVideo,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val defaultModifier = Modifier.fillMaxSize().aspectRatio(1f)
|
||||
|
||||
val automaticallyStartPlayback =
|
||||
remember(content) {
|
||||
mutableStateOf<Boolean>(accountViewModel.settings.startVideoPlayback.value)
|
||||
}
|
||||
|
||||
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(content.url, content.description, content.artworkUri, content.authorName) { mediaItem ->
|
||||
GetVideoController(
|
||||
mediaItem = mediaItem,
|
||||
videoUri = content.url,
|
||||
defaultToStart = true,
|
||||
nostrUriCallback = content.uri,
|
||||
proxyPort = HttpClientManager.getCurrentProxyPort(accountViewModel.account.shouldUseTorForVideoDownload(content.url)),
|
||||
) { controller, keepPlaying ->
|
||||
AndroidView(
|
||||
modifier = Modifier,
|
||||
factory = { context: Context ->
|
||||
PlayerView(context).apply {
|
||||
clipToOutline = true
|
||||
player = controller
|
||||
setShowBuffering(PlayerView.SHOW_BUFFERING_ALWAYS)
|
||||
|
||||
controllerAutoShow = false
|
||||
useController = false
|
||||
|
||||
hideController()
|
||||
|
||||
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL
|
||||
|
||||
controller.playWhenReady = true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
|
||||
@Composable
|
||||
fun RenderGalleryFeed(
|
||||
viewModel: FeedViewModel,
|
||||
listState: LazyGridState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val feedState by viewModel.feedState.feedContent.collectAsStateWithLifecycle()
|
||||
CrossfadeIfEnabled(
|
||||
targetState = feedState,
|
||||
animationSpec = tween(durationMillis = 100),
|
||||
label = "RenderDiscoverFeed",
|
||||
accountViewModel = accountViewModel,
|
||||
) { state ->
|
||||
when (state) {
|
||||
is FeedState.Empty -> {
|
||||
FeedEmpty { viewModel.invalidateData() }
|
||||
}
|
||||
is FeedState.FeedError -> {
|
||||
FeedError(state.errorMessage) { viewModel.invalidateData() }
|
||||
}
|
||||
is FeedState.Loaded -> {
|
||||
GalleryFeedLoaded(
|
||||
state,
|
||||
listState,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
is FeedState.Loading -> {
|
||||
LoadingFeed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GalleryFeedLoaded(
|
||||
loaded: FeedState.Loaded,
|
||||
listState: LazyGridState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
contentPadding = FeedPadding,
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item ->
|
||||
GalleryCardCompose(
|
||||
baseNote = item,
|
||||
modifier =
|
||||
Modifier
|
||||
.aspectRatio(1f)
|
||||
.fillMaxSize()
|
||||
.animateItem()
|
||||
.padding(Size5dp),
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialogOneButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@Composable
|
||||
fun QuickActionGallery(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (() -> Unit) -> Unit,
|
||||
) {
|
||||
val popupExpanded = remember { mutableStateOf(false) }
|
||||
|
||||
content { popupExpanded.value = true }
|
||||
|
||||
if (popupExpanded.value) {
|
||||
if (baseNote.author == accountViewModel.account.userProfile()) {
|
||||
DeleteFromGalleryDialog(
|
||||
note = baseNote,
|
||||
onDismiss = { popupExpanded.value = false },
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteFromGalleryDialog(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
QuickActionAlertDialogOneButton(
|
||||
title = stringRes(R.string.quick_action_request_deletion_gallery_title),
|
||||
textContent = stringRes(R.string.quick_action_request_deletion_gallery_alert_body),
|
||||
buttonIcon = Icons.Default.Delete,
|
||||
buttonText = stringRes(R.string.quick_action_delete_dialog_btn),
|
||||
onClickDoOnce = {
|
||||
accountViewModel.removefromMediaGallery(note)
|
||||
onDismiss()
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user