From ddb1759dfa2180d9bc9f1a8ee5335bd3625bac59 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Wed, 15 Jul 2026 21:42:05 -0300 Subject: [PATCH] feat: show app name byline and bold version pill Drop app ID and channel from card/detail headers; attribute apps with a smaller profile byline and keep a bold blue version pill. --- .../kotlin/dev/zapstore/app/Components.kt | 99 +++++++++++++------ .../dev/zapstore/app/ProfileComponent.kt | 31 ++++-- src/main/kotlin/dev/zapstore/app/Screens.kt | 37 +++---- src/main/res/values/strings.xml | 4 + 4 files changed, 107 insertions(+), 64 deletions(-) diff --git a/src/main/kotlin/dev/zapstore/app/Components.kt b/src/main/kotlin/dev/zapstore/app/Components.kt index 476e8b9..89b677b 100644 --- a/src/main/kotlin/dev/zapstore/app/Components.kt +++ b/src/main/kotlin/dev/zapstore/app/Components.kt @@ -22,10 +22,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +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 @Composable @@ -69,6 +73,8 @@ fun AppCard( release: ReleaseInfo? = null, onProfileClick: (() -> Unit)? = null, ) { + val authorName = rememberProfileDisplayName(app.event.pubKey, repository) + Column( modifier = modifier .fillMaxWidth() @@ -77,42 +83,27 @@ fun AppCard( .clickable(onClick = onClick) .padding(horizontal = 16.dp, vertical = 14.dp), ) { - Row(verticalAlignment = Alignment.CenterVertically) { + Row(verticalAlignment = Alignment.Top) { AppIcon( title = app.name, iconUrl = app.iconUrl, modifier = Modifier.size(52.dp), ) - Spacer(Modifier.width(12.dp)) - Column(Modifier.weight(1f)) { - Text( - text = app.name, - style = MaterialTheme.typography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = buildString { - release?.let { - append(it.version) - it.channel?.let { channel -> append(" · ").append(channel) } - append(" · ") - } - append(app.identifier) - }, - color = ZapMuted, - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.MiddleEllipsis, + Spacer(modifier.width(12.dp)) + Column(modifier.weight(1f)) { + AppNameWithByline( + name = app.name, + authorName = authorName, + nameStyle = MaterialTheme.typography.titleMedium, + onAuthorClick = onProfileClick, + authorTestTag = "profile:${app.event.pubKey}", ) + release?.let { + Spacer(Modifier.height(8.dp)) + VersionPill(version = it.version) + } } } - ProfileComponent( - pubkey = app.event.pubKey, - repository = repository, - modifier = Modifier.padding(top = 10.dp), - onClick = onProfileClick, - ) if (app.summary.isNotBlank()) { Spacer(Modifier.height(10.dp)) Text( @@ -126,6 +117,56 @@ fun AppCard( } } +@Composable +fun AppNameWithByline( + name: String, + authorName: String, + nameStyle: TextStyle, + modifier: Modifier = Modifier, + onAuthorClick: (() -> Unit)? = null, + authorTestTag: String? = null, +) { + val bylineSize = (nameStyle.fontSize.value * 0.72f).sp + Column(modifier = modifier) { + Text( + text = name, + style = nameStyle, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringResource(R.string.app_by_author, authorName), + color = ZapMuted, + style = MaterialTheme.typography.bodySmall.copy( + fontSize = bylineSize, + fontWeight = FontWeight.Normal, + fontFamily = InterFontFamily, + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .then(authorTestTag?.let { Modifier.testTag(it) } ?: Modifier) + .then(onAuthorClick?.let { Modifier.clickable(onClick = it) } ?: Modifier), + ) + } +} + +@Composable +fun VersionPill( + version: String, + modifier: Modifier = Modifier, +) { + Text( + text = version, + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold), + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background(ZapPrimary) + .padding(horizontal = 10.dp, vertical = 4.dp), + ) +} + @Composable fun StackCard( stack: StackInfo, @@ -154,7 +195,7 @@ fun StackCard( maxLines = 2, overflow = TextOverflow.Ellipsis, ) - Spacer(Modifier.height(12.dp)) + Spacer(modifier.height(12.dp)) Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { stack.appAddresses.take(3).forEach { address -> val app = appsByAddress[address] diff --git a/src/main/kotlin/dev/zapstore/app/ProfileComponent.kt b/src/main/kotlin/dev/zapstore/app/ProfileComponent.kt index 4acd50a..9cb4ffa 100644 --- a/src/main/kotlin/dev/zapstore/app/ProfileComponent.kt +++ b/src/main/kotlin/dev/zapstore/app/ProfileComponent.kt @@ -27,12 +27,7 @@ import kotlinx.coroutines.flow.collect import kotlin.time.Duration.Companion.hours @Composable -fun ProfileComponent( - pubkey: String, - repository: CatalogRepository?, - modifier: Modifier = Modifier, - onClick: (() -> Unit)? = null, -) { +fun rememberProfile(pubkey: String, repository: CatalogRepository?): ProfileInfo? { val profile by produceState(initialValue = null, pubkey, repository) { if (repository == null) return@produceState @@ -45,12 +40,30 @@ fun ProfileComponent( value = state.items.maxByOrNull(Event::createdAt)?.let(::ProfileInfo) } } + return profile +} - val npub = pubkey.toNpub() - val displayName = profile?.displayName +fun profileDisplayName(profile: ProfileInfo?, pubkey: String): String = + profile?.displayName ?.takeIf(String::isNotBlank) ?: profile?.name?.takeIf(String::isNotBlank) - ?: npub + ?: pubkey.toNpub() + +@Composable +fun rememberProfileDisplayName( + pubkey: String, + repository: CatalogRepository?, +): String = profileDisplayName(rememberProfile(pubkey, repository), pubkey) + +@Composable +fun ProfileComponent( + pubkey: String, + repository: CatalogRepository?, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, +) { + val profile = rememberProfile(pubkey, repository) + val displayName = profileDisplayName(profile, pubkey) val pictureUrl = profile?.picture?.takeIf(::isHttpUrl) Row( diff --git a/src/main/kotlin/dev/zapstore/app/Screens.kt b/src/main/kotlin/dev/zapstore/app/Screens.kt index b0d4f11..60b4d6c 100644 --- a/src/main/kotlin/dev/zapstore/app/Screens.kt +++ b/src/main/kotlin/dev/zapstore/app/Screens.kt @@ -296,44 +296,29 @@ fun AppDetailScreen( } item { + val authorName = rememberProfileDisplayName(app.event.pubKey, repository) Row(verticalAlignment = Alignment.Top) { AppIcon( title = app.name, iconUrl = app.iconUrl, modifier = Modifier.size(84.dp), ) - Spacer(Modifier.width(16.dp)) - Column(Modifier.weight(1f)) { - Text( - text = app.name, - style = MaterialTheme.typography.displaySmall, + Spacer(modifier.width(16.dp)) + Column(modifier.weight(1f)) { + AppNameWithByline( + name = app.name, + authorName = authorName, + nameStyle = MaterialTheme.typography.displaySmall, + onAuthorClick = { onProfileClick(app.event.pubKey) }, + authorTestTag = "profile:${app.event.pubKey}", ) - StatusText(app.identifier) state.release?.let { release -> - Spacer(Modifier.height(10.dp)) - Text( - text = buildString { - append(release.version) - release.channel?.let { append(" · ").append(it) } - }, - color = MaterialTheme.colorScheme.onPrimary, - style = MaterialTheme.typography.labelMedium, - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background(ZapPrimary) - .padding(horizontal = 10.dp, vertical = 4.dp), - ) + Spacer(modifier.height(10.dp)) + VersionPill(version = release.version) } } } } - item { - ProfileComponent( - pubkey = app.event.pubKey, - repository = repository, - onClick = { onProfileClick(app.event.pubKey) }, - ) - } if (app.screenshots.isNotEmpty()) { item { diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 8670a71..ea184fd 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -2,6 +2,8 @@ Zapstore Search apps Clear search + Notifications + You are all caught up. Curated stacks Latest releases No releases found. @@ -18,7 +20,9 @@ Apps by this profile No apps published by this profile. Published by · %1$s… + by %1$s Latest release + Read more Version: Source License