diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 4b06ffa2f..0cf857f8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index feff10462..fb6940ec7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileGallery.kt deleted file mode 100644 index 436e8cb78..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileGallery.kt +++ /dev/null @@ -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? = 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? = 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), - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt index dfb306d1d..0712b337f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt @@ -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() - 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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt new file mode 100644 index 000000000..593850903 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt @@ -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() } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt new file mode 100644 index 000000000..ad0bac384 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt @@ -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(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 + } + }, + ) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt new file mode 100644 index 000000000..df7e5a2bb --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt @@ -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, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt new file mode 100644 index 000000000..f49fe889d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt @@ -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, + ) +}