From 41055c7769690b32f8a91d6a7c82ceeaa813a996 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 15:36:35 +0000 Subject: [PATCH 1/6] fix: align app detail screen with thread UI conventions - Add 12dp horizontal padding to all app detail sections (header, screenshots, about, platforms, topics, links, releases) so content no longer touches the screen edges like other screens - Render comments with the reply-level indentation bars used by the regular thread feed (drawReplyLevel + levelFlowForItem) - Move the reactions row below the releases, matching the master-note layout of thread screens, and follow it with a divider - Drop the Comments section label since the reactions row + threaded replies already follow the standard screen pattern https://claude.ai/code/session_01RJSke6d4dpSZgdtXAmGMmp --- .../amethyst/ui/note/types/SoftwareApp.kt | 7 +- .../softwareapps/SoftwareAppDetailScreen.kt | 85 ++++++++++++------- 2 files changed, 58 insertions(+), 34 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt index 2dd2480dfc..ffa7d7ba91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -284,10 +285,14 @@ fun TopicChipFlow( } @Composable -fun ScreenshotsStrip(images: List) { +fun ScreenshotsStrip( + images: List, + contentPadding: PaddingValues = PaddingValues(0.dp), +) { if (images.isEmpty()) return LazyRow( horizontalArrangement = Arrangement.spacedBy(6.dp), + contentPadding = contentPadding, modifier = Modifier.height(180.dp), ) { items(images) { imageUrl -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt index 542d091ba7..6720cf2e2f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -54,6 +55,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState +import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription @@ -83,6 +85,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.Thre import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.PaddingHorizontal12Modifier import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.grayText @@ -209,7 +212,7 @@ private fun SoftwareAppDetailBody( if (images.isNotEmpty()) { item(key = "screenshots") { Spacer(Modifier.height(12.dp)) - ScreenshotsStrip(images) + ScreenshotsStrip(images, contentPadding = PaddingValues(horizontal = 12.dp)) } } @@ -252,28 +255,18 @@ private fun SoftwareAppDetailBody( } } - item(key = "reactions") { - Spacer(Modifier.height(12.dp)) - ReactionsRow( - baseNote = note, - showReactionDetail = true, - addPadding = false, - editState = null, - accountViewModel = accountViewModel, - nav = nav, - ) - } - if (latestRelease != null) { item(key = "latest-release") { Spacer(Modifier.height(12.dp)) - SectionLabel(stringRes(R.string.nip82_section_latest_release)) - RenderSoftwareReleaseBody( - event = latestRelease, - accountViewModel = accountViewModel, - nav = nav, - showAppId = false, - ) + Column(PaddingHorizontal12Modifier) { + SectionLabel(stringRes(R.string.nip82_section_latest_release)) + RenderSoftwareReleaseBody( + event = latestRelease, + accountViewModel = accountViewModel, + nav = nav, + showAppId = false, + ) + } } } @@ -292,25 +285,39 @@ private fun SoftwareAppDetailBody( key = { "older-${it.id}" }, ) { release -> Spacer(Modifier.height(8.dp)) - RenderSoftwareReleaseBody( - event = release, - accountViewModel = accountViewModel, - nav = nav, - showAppId = false, - ) + Column(PaddingHorizontal12Modifier) { + RenderSoftwareReleaseBody( + event = release, + accountViewModel = accountViewModel, + nav = nav, + showAppId = false, + ) + } } } } - item(key = "comments-header") { - Spacer(Modifier.height(16.dp)) - SectionLabel(stringRes(R.string.nip82_section_comments)) - if (comments.isEmpty()) { - Spacer(Modifier.height(4.dp)) + item(key = "reactions") { + Spacer(Modifier.height(12.dp)) + ReactionsRow( + baseNote = note, + showReactionDetail = true, + addPadding = true, + editState = null, + accountViewModel = accountViewModel, + nav = nav, + ) + HorizontalDivider(thickness = DividerThickness) + } + + if (comments.isEmpty()) { + item(key = "no-comments") { + Spacer(Modifier.height(8.dp)) Text( text = stringRes(R.string.nip82_no_comments), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.placeholderText, + modifier = PaddingHorizontal12Modifier, ) } } @@ -320,8 +327,16 @@ private fun SoftwareAppDetailBody( key = { _, item -> item.idHex }, contentType = { _, _ -> "comment" }, ) { _, item -> + val level = threadViewModel.levelFlowForItem(item).collectAsStateWithLifecycle(0) + NoteCompose( baseNote = item, + modifier = + Modifier.drawReplyLevel( + level = level, + color = MaterialTheme.colorScheme.placeholderText, + selected = MaterialTheme.colorScheme.placeholderText, + ), isBoostedNote = false, unPackReply = ReplyRenderType.NONE, quotesLeft = 3, @@ -340,7 +355,10 @@ private fun AppDetailHeader( summary: String?, latestVersion: String?, ) { - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = PaddingHorizontal12Modifier, + ) { AppIcon(icon = icon, name = name, sizeDp = 72) Spacer(Modifier.width(14.dp)) Column(Modifier.weight(1f)) { @@ -374,7 +392,7 @@ private fun Section( title: String, content: @Composable () -> Unit, ) { - Column { + Column(PaddingHorizontal12Modifier) { SectionLabel(title) Spacer(Modifier.height(6.dp)) content() @@ -401,6 +419,7 @@ private fun OlderReleasesToggle( modifier = Modifier .fillMaxWidth() + .padding(horizontal = 12.dp) .clip(QuoteBorder) .border(1.dp, MaterialTheme.colorScheme.subtleBorder, QuoteBorder) .clickable { onToggle() } From b9a1d198c29a4c4963879ecb7d04fb5c0533628d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 17:26:27 +0000 Subject: [PATCH 2/6] feat: show app author on the app detail screen Adds a clickable author row (profile picture + username) below the app header, matching the owner row pattern used by community and long-form screens. https://claude.ai/code/session_01RJSke6d4dpSZgdtXAmGMmp --- .../softwareapps/SoftwareAppDetailScreen.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt index 6720cf2e2f..14fb4b9501 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt @@ -66,7 +66,9 @@ import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteCompose +import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.types.AppIcon import com.vitorpamplona.amethyst.ui.note.types.AppLinksColumn @@ -87,6 +89,7 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.PaddingHorizontal12Modifier import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -209,6 +212,18 @@ private fun SoftwareAppDetailBody( ) } + item(key = "author") { + Spacer(Modifier.height(12.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = PaddingHorizontal12Modifier, + ) { + NoteAuthorPicture(note, Size25dp, accountViewModel = accountViewModel, nav = nav) + Spacer(Modifier.width(8.dp)) + NoteUsernameDisplay(note, Modifier.weight(1f), accountViewModel = accountViewModel) + } + } + if (images.isNotEmpty()) { item(key = "screenshots") { Spacer(Modifier.height(12.dp)) From 7743d2a82a335e81216354ba53457e2a8909e2de Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 18:03:58 +0000 Subject: [PATCH 3/6] feat: show 'by ' under the app name in feed cards and detail header Replaces the standalone author row on the detail screen with a shared AppAuthorLine ('by' + clickable profile picture + username) rendered right under the app name, and adds the same line to the app feed cards. https://claude.ai/code/session_01RJSke6d4dpSZgdtXAmGMmp --- .../amethyst/ui/note/types/SoftwareApp.kt | 28 +++++++++++++++++++ .../softwareapps/SoftwareAppDetailScreen.kt | 23 ++++++--------- amethyst/src/main/res/values/strings.xml | 1 + 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt index ffa7d7ba91..c59798276c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt @@ -67,13 +67,17 @@ import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.LinkIcon +import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture +import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.Size16Modifier +import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.subtleBorder import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent @@ -134,6 +138,7 @@ fun RenderSoftwareApplication( overflow = TextOverflow.Ellipsis, ) } + AppAuthorLine(note, accountViewModel, nav) summary?.takeIf { it.isNotBlank() }?.let { Text( text = it, @@ -249,6 +254,29 @@ fun AppIcon( } } +/** + * "by " line with a small clickable profile picture. Shared between + * the app feed card and the app detail screen header. + */ +@Composable +fun AppAuthorLine( + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringRes(R.string.nip82_by_author), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.grayText, + ) + Spacer(Modifier.width(4.dp)) + NoteAuthorPicture(note, Size20dp, accountViewModel = accountViewModel, nav = nav) + Spacer(Modifier.width(4.dp)) + NoteUsernameDisplay(note, Modifier.weight(1f, fill = false), accountViewModel = accountViewModel) + } +} + @OptIn(ExperimentalLayoutApi::class) @Composable fun PlatformLicenseRow( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt index 14fb4b9501..32a7aaaaf3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt @@ -66,10 +66,9 @@ import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote -import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteCompose -import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.ReactionsRow +import com.vitorpamplona.amethyst.ui.note.types.AppAuthorLine import com.vitorpamplona.amethyst.ui.note.types.AppIcon import com.vitorpamplona.amethyst.ui.note.types.AppLinksColumn import com.vitorpamplona.amethyst.ui.note.types.Chip @@ -89,7 +88,6 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.PaddingHorizontal12Modifier import com.vitorpamplona.amethyst.ui.theme.QuoteBorder -import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -205,25 +203,16 @@ private fun SoftwareAppDetailBody( ) { item(key = "header") { AppDetailHeader( + note = note, icon = icon, name = name, summary = summary, latestVersion = latestRelease?.version(), + accountViewModel = accountViewModel, + nav = nav, ) } - item(key = "author") { - Spacer(Modifier.height(12.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = PaddingHorizontal12Modifier, - ) { - NoteAuthorPicture(note, Size25dp, accountViewModel = accountViewModel, nav = nav) - Spacer(Modifier.width(8.dp)) - NoteUsernameDisplay(note, Modifier.weight(1f), accountViewModel = accountViewModel) - } - } - if (images.isNotEmpty()) { item(key = "screenshots") { Spacer(Modifier.height(12.dp)) @@ -365,10 +354,13 @@ private fun SoftwareAppDetailBody( @Composable private fun AppDetailHeader( + note: AddressableNote, icon: String?, name: String, summary: String?, latestVersion: String?, + accountViewModel: AccountViewModel, + nav: INav, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -384,6 +376,7 @@ private fun AppDetailHeader( maxLines = 2, overflow = TextOverflow.Ellipsis, ) + AppAuthorLine(note, accountViewModel, nav) summary?.takeIf { it.isNotBlank() }?.let { Spacer(StdVertSpacer) Text( diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index d24f306852..940be4fa04 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -705,6 +705,7 @@ Latest release Comments Be the first to comment. + by Show older releases Hide older releases Calendars From 7072da528606c116c47ff27650bf0b34a44417c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 18:07:24 +0000 Subject: [PATCH 4/6] feat: show screenshots in app feed cards, drop platform chips Feed cards now render the app's screenshots in a horizontal LazyRow at 200dp so the images are big enough to convey what the app is about. The platform/license chips were removed from the card since they add little at feed level (still shown on the detail screen). https://claude.ai/code/session_01RJSke6d4dpSZgdtXAmGMmp --- .../amethyst/ui/note/types/SoftwareApp.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt index c59798276c..6feb01fa7f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage @@ -108,8 +109,7 @@ fun RenderSoftwareApplication( val name = remember(event) { event.name() ?: event.appId().orEmpty() } val summary = remember(event) { event.summary() } val description = remember(event) { event.content.trim() } - val platforms = remember(event) { event.platforms() } - val license = remember(event) { event.license() } + val images = remember(event) { event.images() } val latestVersion by produceLatestReleaseVersion(event) @@ -166,9 +166,9 @@ fun RenderSoftwareApplication( ) } - if (platforms.isNotEmpty() || license != null) { + if (images.isNotEmpty()) { Spacer(StdVertSpacer) - PlatformLicenseRow(platforms = platforms, license = license) + ScreenshotsStrip(images, imageHeight = 200.dp) } } @@ -316,12 +316,13 @@ fun TopicChipFlow( fun ScreenshotsStrip( images: List, contentPadding: PaddingValues = PaddingValues(0.dp), + imageHeight: Dp = 180.dp, ) { if (images.isEmpty()) return LazyRow( horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = contentPadding, - modifier = Modifier.height(180.dp), + modifier = Modifier.height(imageHeight), ) { items(images) { imageUrl -> AsyncImage( @@ -330,7 +331,7 @@ fun ScreenshotsStrip( contentScale = ContentScale.Crop, modifier = Modifier - .height(180.dp) + .height(imageHeight) .clip(RoundedCornerShape(8.dp)) .border(1.dp, MaterialTheme.colorScheme.subtleBorder, RoundedCornerShape(8.dp)), ) From 8c7430038d645f556887c3fa4ec66c9c16ca07a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 18:19:20 +0000 Subject: [PATCH 5/6] fix: letter-avatar fallback for app icons while loading or on failure AppIcon used to render an empty bordered box when the icon url was missing, still downloading, or failed to load. It now draws the app name's first letter on a surfaceVariant background underneath the image, so all three states show a meaningful placeholder. https://claude.ai/code/session_01RJSke6d4dpSZgdtXAmGMmp --- .../amethyst/ui/note/types/SoftwareApp.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt index 6feb01fa7f..6f8f4e5a3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt @@ -237,12 +237,24 @@ fun AppIcon( name: String, sizeDp: Int = 56, ) { + val shape = RoundedCornerShape((sizeDp / 4).dp) Box( Modifier .size(sizeDp.dp) - .clip(RoundedCornerShape((sizeDp / 4).dp)) - .border(1.dp, MaterialTheme.colorScheme.subtleBorder, RoundedCornerShape((sizeDp / 4).dp)), + .clip(shape) + .background(MaterialTheme.colorScheme.surfaceVariant) + .border(1.dp, MaterialTheme.colorScheme.subtleBorder, shape), + contentAlignment = Alignment.Center, ) { + // Fallback underneath the image: visible when there is no icon url, + // while the icon downloads, and when the download fails (AsyncImage + // draws nothing in those states). + Text( + text = (name.firstOrNull() ?: '?').uppercase(), + fontSize = (sizeDp / 2).sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.grayText, + ) icon?.let { AsyncImage( model = it, From bb43fd97b804ae19387fbbc242d5d676e87a5e78 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 19:31:31 +0000 Subject: [PATCH 6/6] feat: open app screenshots fullscreen via ZoomableContentView ScreenshotsStrip now renders each screenshot through ZoomableContentView, so tapping one opens the standard fullscreen zoomable viewer with paging across all of the app's screenshots. Tiles keep a fixed height and take their width from the image's cached aspect ratio (portrait fallback before first load). https://claude.ai/code/session_01RJSke6d4dpSZgdtXAmGMmp --- .../amethyst/ui/note/types/SoftwareApp.kt | 45 ++++++++++++++----- .../softwareapps/SoftwareAppDetailScreen.kt | 2 +- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt index 6f8f4e5a3f..65f9e9495d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt @@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -60,11 +61,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.MediaAspectRatioCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.filterIntoSet import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary +import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.LinkIcon @@ -87,6 +91,7 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.SoftwareR import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.asSoftwareRelease import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -168,7 +173,7 @@ fun RenderSoftwareApplication( if (images.isNotEmpty()) { Spacer(StdVertSpacer) - ScreenshotsStrip(images, imageHeight = 200.dp) + ScreenshotsStrip(images, accountViewModel, imageHeight = 200.dp) } } @@ -327,26 +332,42 @@ fun TopicChipFlow( @Composable fun ScreenshotsStrip( images: List, + accountViewModel: AccountViewModel, contentPadding: PaddingValues = PaddingValues(0.dp), imageHeight: Dp = 180.dp, ) { if (images.isEmpty()) return + + val mediaContents = + remember(images) { + images.map { MediaUrlImage(url = it) }.toImmutableList() + } + LazyRow( horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = contentPadding, modifier = Modifier.height(imageHeight), ) { - items(images) { imageUrl -> - AsyncImage( - model = imageUrl, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = - Modifier - .height(imageHeight) - .clip(RoundedCornerShape(8.dp)) - .border(1.dp, MaterialTheme.colorScheme.subtleBorder, RoundedCornerShape(8.dp)), - ) + items(mediaContents) { content -> + // Fixed-height tile whose width follows the image's aspect ratio + // once it is known; assumes a portrait phone screenshot before the + // first load fills the ratio cache. + val ratio = MediaAspectRatioCache.get(content.url) ?: (9f / 16f) + Box( + Modifier + .height(imageHeight) + .aspectRatio(ratio) + .clip(RoundedCornerShape(8.dp)) + .border(1.dp, MaterialTheme.colorScheme.subtleBorder, RoundedCornerShape(8.dp)), + ) { + ZoomableContentView( + content = content, + images = mediaContents, + roundedCorner = false, + contentScale = ContentScale.Crop, + accountViewModel = accountViewModel, + ) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt index 32a7aaaaf3..5a7f142b21 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt @@ -216,7 +216,7 @@ private fun SoftwareAppDetailBody( if (images.isNotEmpty()) { item(key = "screenshots") { Spacer(Modifier.height(12.dp)) - ScreenshotsStrip(images, contentPadding = PaddingValues(horizontal = 12.dp)) + ScreenshotsStrip(images, accountViewModel, contentPadding = PaddingValues(horizontal = 12.dp)) } }