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.
This commit is contained in:
franzap
2026-07-15 21:42:05 -03:00
parent 8146a40994
commit ddb1759dfa
4 changed files with 107 additions and 64 deletions
+70 -29
View File
@@ -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]
@@ -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<ProfileInfo?>(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(
+11 -26
View File
@@ -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 {
+4
View File
@@ -2,6 +2,8 @@
<string name="app_name">Zapstore</string>
<string name="search_apps">Search apps</string>
<string name="clear_search">Clear search</string>
<string name="notifications">Notifications</string>
<string name="no_notifications">You are all caught up.</string>
<string name="curated_stacks">Curated stacks</string>
<string name="latest_releases">Latest releases</string>
<string name="no_releases">No releases found.</string>
@@ -18,7 +20,9 @@
<string name="profile_apps">Apps by this profile</string>
<string name="no_profile_apps">No apps published by this profile.</string>
<string name="published_by">Published by · %1$s…</string>
<string name="app_by_author">by %1$s</string>
<string name="latest_release">Latest release</string>
<string name="read_more">Read more</string>
<string name="version">Version:</string>
<string name="source">Source</string>
<string name="license">License</string>