Loading app/build.gradle +1 −0 Original line number Diff line number Diff line Loading @@ -315,6 +315,7 @@ dependencies { // Coil and PhotoView implementation(libs.coil) implementation(libs.coil.compose) implementation(libs.photoview) // Protobuf and Gson Loading app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt 0 → 100644 +442 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.ui.compose.components import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Star import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.rememberImagePainter import foundation.e.apps.R import foundation.e.apps.data.application.data.Application import foundation.e.apps.ui.compose.theme.AppTheme @Composable fun SearchResultListItem( application: Application, uiState: SearchResultListItemState, onItemClick: (Application) -> Unit, onPrimaryActionClick: (Application) -> Unit, onShowMoreClick: (Application) -> Unit, onPrivacyClick: (Application) -> Unit, modifier: Modifier = Modifier, ) { if (uiState.isPlaceholder) { PlaceholderRow(modifier = modifier) return } else { // fall through to render the normal row } val interactionSource = remember { MutableInteractionSource() } Row( modifier = modifier .fillMaxWidth() .clickable( interactionSource = interactionSource, indication = null, onClick = { onItemClick(application) }, ) .padding(horizontal = 12.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { AppIcon( imageUrl = uiState.iconUrl, contentDescription = application.name, placeholderPainterRes = uiState.placeholderResId, ) Column( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp), ) { Text( text = application.name, style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold), color = MaterialTheme.colorScheme.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, ) Text( text = uiState.author, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.72f), maxLines = 1, overflow = TextOverflow.Ellipsis, ) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), ) { if (uiState.showRating) { RatingChip(ratingText = uiState.ratingText) } else { // keep layout predictable; hide rating when absent Spacer(modifier = Modifier.width(0.dp)) } if (uiState.showSourceTag) { SourceTag(text = uiState.sourceTag) } else { // design PNG omits source tag; kept togglable for legacy parity Spacer(modifier = Modifier.width(0.dp)) } } } PrimaryActionArea( uiState = uiState.primaryAction, onPrimaryClick = { onPrimaryActionClick(application) }, onShowMoreClick = { onShowMoreClick(application) }, privacyScore = uiState.privacyScore, showPrivacyScore = uiState.showPrivacyScore, isPrivacyLoading = uiState.isPrivacyLoading, onPrivacyClick = { onPrivacyClick(application) }, ) } } @Composable private fun AppIcon( imageUrl: String?, contentDescription: String, placeholderPainterRes: Int?, ) { val painter = rememberImagePainter( data = imageUrl, builder = { placeholderPainterRes?.let { placeholder(it) } placeholderPainterRes?.let { error(it) } placeholderPainterRes?.let { fallback(it) } }, ) Image( painter = painter, contentDescription = contentDescription, modifier = Modifier .size(64.dp) .clip(RoundedCornerShape(12.dp)) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), ) } @Composable private fun RatingChip(ratingText: String) { Row(verticalAlignment = Alignment.CenterVertically) { Image( imageVector = Icons.Rounded.Star, contentDescription = stringResource(id = R.string.rating), modifier = Modifier.size(18.dp), colorFilter = ColorFilter.tint(Color(0xFFFBC02D)), ) Spacer(modifier = Modifier.width(4.dp)) Text( text = ratingText, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onBackground, ) } } @Composable private fun SourceTag(text: String) { Text( text = text, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier .background( color = MaterialTheme.colorScheme.secondaryContainer, shape = MaterialTheme.shapes.small, ) .padding(horizontal = 8.dp, vertical = 4.dp), maxLines = 1, overflow = TextOverflow.Ellipsis, ) } @Composable private fun PrivacyBadge( privacyScore: String, isVisible: Boolean, isLoading: Boolean, onClick: () -> Unit, ) { if (!isVisible) { return } else { // proceed to render the badge } Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable(onClick = onClick), ) { Image( painter = painterResource(id = R.drawable.ic_lock), contentDescription = stringResource(id = R.string.privacy_score), modifier = Modifier.size(16.dp), ) Spacer(modifier = Modifier.width(4.dp)) if (isLoading) { CircularProgressIndicator( modifier = Modifier.size(16.dp), strokeWidth = 2.dp, ) } else { Text( text = privacyScore, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onBackground, ) } } } @Composable private fun PrimaryActionArea( uiState: PrimaryActionUiState, onPrimaryClick: () -> Unit, onShowMoreClick: () -> Unit, privacyScore: String, showPrivacyScore: Boolean, isPrivacyLoading: Boolean, onPrivacyClick: () -> Unit, ) { if (uiState.showMore) { Text( text = stringResource(id = R.string.show_more), style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold), color = MaterialTheme.colorScheme.primary, modifier = Modifier.clickable(onClick = onShowMoreClick), ) return } else { // render the primary action button } val buttonContent: @Composable () -> Unit = { if (uiState.isInProgress) { val indicatorColor = if (uiState.isFilledStyle) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onPrimary CircularProgressIndicator( modifier = Modifier.size(16.dp), strokeWidth = 2.dp, color = indicatorColor, ) } else { val textColor = if (uiState.isFilledStyle) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface Text( text = uiState.label, maxLines = 1, overflow = TextOverflow.Clip, color = textColor, ) } } Column(horizontalAlignment = Alignment.End) { val containerColor = if (uiState.isFilledStyle) { MaterialTheme.colorScheme.primary } else { MaterialTheme.colorScheme.secondaryContainer } val contentColor = if (uiState.isFilledStyle) { MaterialTheme.colorScheme.onPrimary } else { MaterialTheme.colorScheme.onSecondaryContainer } Button( onClick = onPrimaryClick, enabled = uiState.enabled, modifier = Modifier.height(40.dp), shape = RoundedCornerShape(4.dp), colors = ButtonDefaults.buttonColors( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = containerColor.copy(alpha = 0.38f), disabledContentColor = contentColor.copy(alpha = 0.38f), ), contentPadding = ButtonDefaults.ContentPadding, ) { buttonContent() } if (showPrivacyScore) { Spacer(modifier = Modifier.height(8.dp)) PrivacyBadge( privacyScore = privacyScore, isVisible = true, isLoading = isPrivacyLoading, onClick = onPrivacyClick, ) } } } @Composable private fun PlaceholderRow(modifier: Modifier = Modifier) { Box( modifier = modifier .fillMaxWidth() .padding(vertical = 16.dp), contentAlignment = Alignment.Center, ) { CircularProgressIndicator() } } data class SearchResultListItemState( val author: String, val ratingText: String, val showRating: Boolean, val sourceTag: String, val showSourceTag: Boolean, val privacyScore: String, val showPrivacyScore: Boolean, val isPrivacyLoading: Boolean, val primaryAction: PrimaryActionUiState, val iconUrl: String? = null, val placeholderResId: Int?, val isPlaceholder: Boolean = false, ) data class PrimaryActionUiState( val label: String, val enabled: Boolean, val isInProgress: Boolean, val isFilledStyle: Boolean, val showMore: Boolean = false, ) // --- Previews --- @Preview(showBackground = true) @Composable private fun SearchResultListItemPreviewInstall() { AppTheme(darkTheme = true) { Surface(color = MaterialTheme.colorScheme.background) { SearchResultListItem( application = sampleApp(name = "iMe: AI Messenger"), uiState = sampleState( rating = "4.4", privacy = "06/10", primary = PrimaryActionUiState( label = "Install", enabled = true, isInProgress = false, isFilledStyle = false, showMore = false, ), ), onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, onPrivacyClick = {}, ) } } } @Preview(showBackground = true) @Composable private fun SearchResultListItemPreviewOpen() { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { SearchResultListItem( application = sampleApp(name = "This is a very long app name"), uiState = sampleState( rating = "4.3", privacy = "10/10", primary = PrimaryActionUiState( label = "Open", enabled = true, isInProgress = false, isFilledStyle = true, showMore = false, ), ), onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, onPrivacyClick = {}, ) } } } private fun sampleApp(name: String) = Application(name = name) @Composable private fun sampleState( rating: String, privacy: String, primary: PrimaryActionUiState, ): SearchResultListItemState = SearchResultListItemState( author = "This is a very long author name which can take multiple lines", ratingText = rating, showRating = true, sourceTag = "Open-source", // PNG omits this; kept for legacy data showSourceTag = true, privacyScore = privacy, showPrivacyScore = true, isPrivacyLoading = false, primaryAction = primary, isPlaceholder = false, iconUrl = null, placeholderResId = null, ) gradle/libs.versions.toml +2 −0 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ androidGradlePlugin = "8.9.3" appcompat = "1.7.0" bcpgJdk15on = "1.60" coil = "1.4.0" coilCompose = "1.4.0" composeBom = "2025.12.01" constraintlayout = "2.2.0" core = "1.6.1" Loading Loading @@ -62,6 +63,7 @@ activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activ appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } bcpg-jdk15on = { module = "org.bouncycastle:bcpg-jdk15on", version.ref = "bcpgJdk15on" } coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } compose-material3 = { module = "androidx.compose.material3:material3" } compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } Loading Loading
app/build.gradle +1 −0 Original line number Diff line number Diff line Loading @@ -315,6 +315,7 @@ dependencies { // Coil and PhotoView implementation(libs.coil) implementation(libs.coil.compose) implementation(libs.photoview) // Protobuf and Gson Loading
app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt 0 → 100644 +442 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.ui.compose.components import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Star import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.rememberImagePainter import foundation.e.apps.R import foundation.e.apps.data.application.data.Application import foundation.e.apps.ui.compose.theme.AppTheme @Composable fun SearchResultListItem( application: Application, uiState: SearchResultListItemState, onItemClick: (Application) -> Unit, onPrimaryActionClick: (Application) -> Unit, onShowMoreClick: (Application) -> Unit, onPrivacyClick: (Application) -> Unit, modifier: Modifier = Modifier, ) { if (uiState.isPlaceholder) { PlaceholderRow(modifier = modifier) return } else { // fall through to render the normal row } val interactionSource = remember { MutableInteractionSource() } Row( modifier = modifier .fillMaxWidth() .clickable( interactionSource = interactionSource, indication = null, onClick = { onItemClick(application) }, ) .padding(horizontal = 12.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { AppIcon( imageUrl = uiState.iconUrl, contentDescription = application.name, placeholderPainterRes = uiState.placeholderResId, ) Column( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp), ) { Text( text = application.name, style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold), color = MaterialTheme.colorScheme.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, ) Text( text = uiState.author, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.72f), maxLines = 1, overflow = TextOverflow.Ellipsis, ) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), ) { if (uiState.showRating) { RatingChip(ratingText = uiState.ratingText) } else { // keep layout predictable; hide rating when absent Spacer(modifier = Modifier.width(0.dp)) } if (uiState.showSourceTag) { SourceTag(text = uiState.sourceTag) } else { // design PNG omits source tag; kept togglable for legacy parity Spacer(modifier = Modifier.width(0.dp)) } } } PrimaryActionArea( uiState = uiState.primaryAction, onPrimaryClick = { onPrimaryActionClick(application) }, onShowMoreClick = { onShowMoreClick(application) }, privacyScore = uiState.privacyScore, showPrivacyScore = uiState.showPrivacyScore, isPrivacyLoading = uiState.isPrivacyLoading, onPrivacyClick = { onPrivacyClick(application) }, ) } } @Composable private fun AppIcon( imageUrl: String?, contentDescription: String, placeholderPainterRes: Int?, ) { val painter = rememberImagePainter( data = imageUrl, builder = { placeholderPainterRes?.let { placeholder(it) } placeholderPainterRes?.let { error(it) } placeholderPainterRes?.let { fallback(it) } }, ) Image( painter = painter, contentDescription = contentDescription, modifier = Modifier .size(64.dp) .clip(RoundedCornerShape(12.dp)) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), ) } @Composable private fun RatingChip(ratingText: String) { Row(verticalAlignment = Alignment.CenterVertically) { Image( imageVector = Icons.Rounded.Star, contentDescription = stringResource(id = R.string.rating), modifier = Modifier.size(18.dp), colorFilter = ColorFilter.tint(Color(0xFFFBC02D)), ) Spacer(modifier = Modifier.width(4.dp)) Text( text = ratingText, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onBackground, ) } } @Composable private fun SourceTag(text: String) { Text( text = text, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier .background( color = MaterialTheme.colorScheme.secondaryContainer, shape = MaterialTheme.shapes.small, ) .padding(horizontal = 8.dp, vertical = 4.dp), maxLines = 1, overflow = TextOverflow.Ellipsis, ) } @Composable private fun PrivacyBadge( privacyScore: String, isVisible: Boolean, isLoading: Boolean, onClick: () -> Unit, ) { if (!isVisible) { return } else { // proceed to render the badge } Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable(onClick = onClick), ) { Image( painter = painterResource(id = R.drawable.ic_lock), contentDescription = stringResource(id = R.string.privacy_score), modifier = Modifier.size(16.dp), ) Spacer(modifier = Modifier.width(4.dp)) if (isLoading) { CircularProgressIndicator( modifier = Modifier.size(16.dp), strokeWidth = 2.dp, ) } else { Text( text = privacyScore, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onBackground, ) } } } @Composable private fun PrimaryActionArea( uiState: PrimaryActionUiState, onPrimaryClick: () -> Unit, onShowMoreClick: () -> Unit, privacyScore: String, showPrivacyScore: Boolean, isPrivacyLoading: Boolean, onPrivacyClick: () -> Unit, ) { if (uiState.showMore) { Text( text = stringResource(id = R.string.show_more), style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold), color = MaterialTheme.colorScheme.primary, modifier = Modifier.clickable(onClick = onShowMoreClick), ) return } else { // render the primary action button } val buttonContent: @Composable () -> Unit = { if (uiState.isInProgress) { val indicatorColor = if (uiState.isFilledStyle) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onPrimary CircularProgressIndicator( modifier = Modifier.size(16.dp), strokeWidth = 2.dp, color = indicatorColor, ) } else { val textColor = if (uiState.isFilledStyle) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface Text( text = uiState.label, maxLines = 1, overflow = TextOverflow.Clip, color = textColor, ) } } Column(horizontalAlignment = Alignment.End) { val containerColor = if (uiState.isFilledStyle) { MaterialTheme.colorScheme.primary } else { MaterialTheme.colorScheme.secondaryContainer } val contentColor = if (uiState.isFilledStyle) { MaterialTheme.colorScheme.onPrimary } else { MaterialTheme.colorScheme.onSecondaryContainer } Button( onClick = onPrimaryClick, enabled = uiState.enabled, modifier = Modifier.height(40.dp), shape = RoundedCornerShape(4.dp), colors = ButtonDefaults.buttonColors( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = containerColor.copy(alpha = 0.38f), disabledContentColor = contentColor.copy(alpha = 0.38f), ), contentPadding = ButtonDefaults.ContentPadding, ) { buttonContent() } if (showPrivacyScore) { Spacer(modifier = Modifier.height(8.dp)) PrivacyBadge( privacyScore = privacyScore, isVisible = true, isLoading = isPrivacyLoading, onClick = onPrivacyClick, ) } } } @Composable private fun PlaceholderRow(modifier: Modifier = Modifier) { Box( modifier = modifier .fillMaxWidth() .padding(vertical = 16.dp), contentAlignment = Alignment.Center, ) { CircularProgressIndicator() } } data class SearchResultListItemState( val author: String, val ratingText: String, val showRating: Boolean, val sourceTag: String, val showSourceTag: Boolean, val privacyScore: String, val showPrivacyScore: Boolean, val isPrivacyLoading: Boolean, val primaryAction: PrimaryActionUiState, val iconUrl: String? = null, val placeholderResId: Int?, val isPlaceholder: Boolean = false, ) data class PrimaryActionUiState( val label: String, val enabled: Boolean, val isInProgress: Boolean, val isFilledStyle: Boolean, val showMore: Boolean = false, ) // --- Previews --- @Preview(showBackground = true) @Composable private fun SearchResultListItemPreviewInstall() { AppTheme(darkTheme = true) { Surface(color = MaterialTheme.colorScheme.background) { SearchResultListItem( application = sampleApp(name = "iMe: AI Messenger"), uiState = sampleState( rating = "4.4", privacy = "06/10", primary = PrimaryActionUiState( label = "Install", enabled = true, isInProgress = false, isFilledStyle = false, showMore = false, ), ), onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, onPrivacyClick = {}, ) } } } @Preview(showBackground = true) @Composable private fun SearchResultListItemPreviewOpen() { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { SearchResultListItem( application = sampleApp(name = "This is a very long app name"), uiState = sampleState( rating = "4.3", privacy = "10/10", primary = PrimaryActionUiState( label = "Open", enabled = true, isInProgress = false, isFilledStyle = true, showMore = false, ), ), onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, onPrivacyClick = {}, ) } } } private fun sampleApp(name: String) = Application(name = name) @Composable private fun sampleState( rating: String, privacy: String, primary: PrimaryActionUiState, ): SearchResultListItemState = SearchResultListItemState( author = "This is a very long author name which can take multiple lines", ratingText = rating, showRating = true, sourceTag = "Open-source", // PNG omits this; kept for legacy data showSourceTag = true, privacyScore = privacy, showPrivacyScore = true, isPrivacyLoading = false, primaryAction = primary, isPlaceholder = false, iconUrl = null, placeholderResId = null, )
gradle/libs.versions.toml +2 −0 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ androidGradlePlugin = "8.9.3" appcompat = "1.7.0" bcpgJdk15on = "1.60" coil = "1.4.0" coilCompose = "1.4.0" composeBom = "2025.12.01" constraintlayout = "2.2.0" core = "1.6.1" Loading Loading @@ -62,6 +63,7 @@ activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activ appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } bcpg-jdk15on = { module = "org.bouncycastle:bcpg-jdk15on", version.ref = "bcpgJdk15on" } coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } compose-material3 = { module = "androidx.compose.material3:material3" } compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } Loading