mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-10-10 01:22:43 +02:00
Move gallery previews to it's own package. Avoid reusing VideoView
This commit is contained in:
@@ -27,7 +27,6 @@ import androidx.compose.animation.fadeOut
|
|||||||
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.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.text.InlineTextContent
|
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.Size75dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark
|
import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark
|
||||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier
|
|
||||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||||
@@ -135,7 +133,10 @@ fun ZoomableContentView(
|
|||||||
is MediaUrlImage ->
|
is MediaUrlImage ->
|
||||||
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
||||||
TwoSecondController(content) { controllerVisible ->
|
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()
|
val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
|
||||||
|
|
||||||
UrlImageView(content, contentScale, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
UrlImageView(content, contentScale, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
||||||
@@ -167,7 +168,10 @@ fun ZoomableContentView(
|
|||||||
}
|
}
|
||||||
is MediaLocalImage ->
|
is MediaLocalImage ->
|
||||||
TwoSecondController(content) { controllerVisible ->
|
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()
|
val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
|
||||||
|
|
||||||
LocalImageView(content, contentScale, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
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
|
@Composable
|
||||||
fun TwoSecondController(
|
fun TwoSecondController(
|
||||||
content: BaseMediaContent,
|
content: BaseMediaContent,
|
||||||
@@ -501,7 +450,12 @@ fun ImageUrlWithDownloadButton(
|
|||||||
|
|
||||||
val inlineContent = mapOf("inlineContent" to InlineDownloadIcon(showImage))
|
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(
|
||||||
text = annotatedTermsString,
|
text = annotatedTermsString,
|
||||||
@@ -555,7 +509,7 @@ fun aspectRatio(dim: Dimension?): Float? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DisplayUrlWithLoadingSymbol(content: BaseMediaContent) {
|
fun DisplayUrlWithLoadingSymbol(content: BaseMediaContent) {
|
||||||
val uri = LocalUriHandler.current
|
val uri = LocalUriHandler.current
|
||||||
|
|
||||||
val primary = MaterialTheme.colorScheme.primary
|
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
|
@Composable
|
||||||
fun NoteQuickActionMenu(
|
fun NoteQuickActionMenu(
|
||||||
note: Note,
|
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
|
@Composable
|
||||||
fun DeleteAlertDialog(
|
fun DeleteAlertDialog(
|
||||||
note: Note,
|
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.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagHeader
|
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.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.screen.loggedIn.qrcode.ShowQRDialog
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
@@ -1603,7 +1604,6 @@ fun TabGallery(
|
|||||||
SaveableGridFeedState(feedViewModel, scrollStateKey = ScrollStateKeys.PROFILE_GALLERY) { listState ->
|
SaveableGridFeedState(feedViewModel, scrollStateKey = ScrollStateKeys.PROFILE_GALLERY) { listState ->
|
||||||
RenderGalleryFeed(
|
RenderGalleryFeed(
|
||||||
feedViewModel,
|
feedViewModel,
|
||||||
null,
|
|
||||||
listState,
|
listState,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
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
|
@Composable
|
||||||
fun TabFollowedTags(
|
fun TabFollowedTags(
|
||||||
baseUser: User,
|
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,
|
||||||
|
)
|
||||||
|
}
|
Reference in New Issue
Block a user