compose: add stream and app item composables
StreamCarousel for cluster-based home/category lists, CategoryItem grid tile, and AppUpdateItem for the updates list. AnimatedAppIcon plus the existing AppListItem and LargeAppListItem are adjusted to fit the new call sites.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.compose.composable
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import com.aurora.gplayapi.data.models.Category
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
|
||||
@Composable
|
||||
fun CategoryItem(modifier: Modifier = Modifier, category: Category, onClick: () -> Unit = {}) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_small)
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_large))
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(category.imageUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.requiredSize(dimensionResource(R.dimen.icon_size_category))
|
||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small)))
|
||||
)
|
||||
|
||||
Text(
|
||||
text = category.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun CategoryItemPreview() {
|
||||
CategoryItem(
|
||||
category = Category(
|
||||
title = "Entertainment",
|
||||
imageUrl = ""
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.compose.composable
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
import com.aurora.gplayapi.data.models.StreamBundle
|
||||
import com.aurora.gplayapi.data.models.StreamCluster
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.composable.app.AppListItem
|
||||
import com.aurora.store.compose.composable.app.LargeAppListItem
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
private const val LOAD_MORE_THRESHOLD = 2
|
||||
|
||||
@Composable
|
||||
fun StreamCarousel(
|
||||
modifier: Modifier = Modifier,
|
||||
streamBundle: StreamBundle?,
|
||||
filterSingleAppClusters: Boolean = true,
|
||||
lazyListState: LazyListState = rememberLazyListState(),
|
||||
onHeaderClick: (StreamCluster) -> Unit = {},
|
||||
onAppClick: (App) -> Unit = {},
|
||||
onAppLongClick: (App) -> Unit = {},
|
||||
onClusterScrolled: (StreamCluster) -> Unit = {},
|
||||
onScrolledToEnd: () -> Unit = {}
|
||||
) {
|
||||
val bundleLoaded = streamBundle != null
|
||||
LaunchedEffect(lazyListState, bundleLoaded) {
|
||||
snapshotFlow {
|
||||
val last = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||
val total = lazyListState.layoutInfo.totalItemsCount
|
||||
last >= total - LOAD_MORE_THRESHOLD
|
||||
}.distinctUntilChanged().collect { reachedEnd ->
|
||||
if (reachedEnd && bundleLoaded) onScrolledToEnd()
|
||||
}
|
||||
}
|
||||
|
||||
if (streamBundle == null) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||
) {
|
||||
items(5) { ShimmerCarouselSection() }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val clusters = streamBundle.streamClusters.values
|
||||
.filter { cluster ->
|
||||
cluster.clusterAppList.isNotEmpty() &&
|
||||
cluster.clusterTitle.isNotBlank() &&
|
||||
(!filterSingleAppClusters || cluster.clusterAppList.size > 1)
|
||||
}
|
||||
|
||||
if (clusters.isEmpty()) {
|
||||
EmptyState(
|
||||
modifier = modifier,
|
||||
icon = R.drawable.ic_apps,
|
||||
message = R.string.no_apps_available
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(
|
||||
if (clusters.size == 1) {
|
||||
R.dimen.margin_medium
|
||||
} else {
|
||||
R.dimen.margin_xxsmall
|
||||
}
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (clusters.size == 1) {
|
||||
val apps = clusters.first().clusterAppList
|
||||
items(count = apps.size, key = { apps[it].id }) { index ->
|
||||
LargeAppListItem(
|
||||
app = apps[index],
|
||||
onClick = { onAppClick(apps[index]) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
clusters.forEach { cluster ->
|
||||
item(key = "header_${cluster.id}") {
|
||||
SectionHeader(
|
||||
title = cluster.clusterTitle,
|
||||
browseUrl = cluster.clusterBrowseUrl,
|
||||
onClick = { onHeaderClick(cluster) }
|
||||
)
|
||||
}
|
||||
item(key = "row_${cluster.id}") {
|
||||
ClusterRow(
|
||||
cluster = cluster,
|
||||
onAppClick = onAppClick,
|
||||
onAppLongClick = onAppLongClick,
|
||||
onClusterScrolled = onClusterScrolled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (streamBundle.hasNext()) {
|
||||
item(key = "shimmer_footer") { ShimmerCarouselSection() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClusterRow(
|
||||
cluster: StreamCluster,
|
||||
onAppClick: (App) -> Unit,
|
||||
onAppLongClick: (App) -> Unit,
|
||||
onClusterScrolled: (StreamCluster) -> Unit
|
||||
) {
|
||||
val rowState = rememberLazyListState()
|
||||
val reachedEnd by remember {
|
||||
derivedStateOf {
|
||||
val last = rowState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||
val total = rowState.layoutInfo.totalItemsCount
|
||||
last >= total - LOAD_MORE_THRESHOLD
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(reachedEnd) {
|
||||
if (reachedEnd && cluster.hasNext()) onClusterScrolled(cluster)
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
state = rowState,
|
||||
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_small)),
|
||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_small))
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = cluster.clusterAppList,
|
||||
key = { _, app -> app.packageName }
|
||||
) { _, app ->
|
||||
AppListItem(
|
||||
app = app,
|
||||
onClick = { onAppClick(app) },
|
||||
onLongClick = { onAppLongClick(app) }
|
||||
)
|
||||
}
|
||||
if (cluster.hasNext()) {
|
||||
item(key = "shimmer_${cluster.id}") { ShimmerAppListItem() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun StreamCarouselLoadingPreview() {
|
||||
StreamCarousel(streamBundle = null)
|
||||
}
|
||||
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun StreamCarouselEmptyPreview() {
|
||||
StreamCarousel(streamBundle = StreamBundle.EMPTY)
|
||||
}
|
||||
@@ -111,7 +111,9 @@ private fun AnimatedAppIconPreview(@PreviewParameter(AppPreviewProvider::class)
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun AnimatedAppIconPreview(@PreviewParameter(ProgressProvider::class) progress: Float) {
|
||||
private fun AnimatedAppIconInProgressPreview(
|
||||
@PreviewParameter(ProgressProvider::class) progress: Float
|
||||
) {
|
||||
AnimatedAppIcon(
|
||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
|
||||
iconUrl = "",
|
||||
|
||||
@@ -5,21 +5,25 @@
|
||||
|
||||
package com.aurora.store.compose.composable.app
|
||||
|
||||
import android.text.format.Formatter
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
@@ -37,42 +41,49 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
* @param modifier The modifier to be applied to the composable
|
||||
* @param app [App] to display
|
||||
* @param onClick Callback when the composable is clicked
|
||||
* @param onLongClick Callback when the composable is long-clicked
|
||||
* @see LargeAppListItem
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun AppListItem(modifier: Modifier = Modifier, app: App, onClick: () -> Unit = {}) {
|
||||
val context = LocalContext.current
|
||||
|
||||
fun AppListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
app: App,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(dimensionResource(R.dimen.icon_size_cluster))
|
||||
.clickable(onClick = onClick)
|
||||
.padding(dimensionResource(R.dimen.padding_xsmall))
|
||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||
.padding(all = dimensionResource(R.dimen.padding_xxsmall)),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.margin_xsmall),
|
||||
Alignment.CenterVertically
|
||||
)
|
||||
) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium))),
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(app.iconArtwork.url)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
contentScale = ContentScale.Crop
|
||||
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.requiredSize(dimensionResource(R.dimen.icon_size_cluster))
|
||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
|
||||
)
|
||||
Text(
|
||||
.fillMaxWidth(),
|
||||
text = app.displayName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = if (app.size > 0) {
|
||||
Formatter.formatShortFileSize(context, app.size)
|
||||
} else {
|
||||
app.downloadString
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.compose.composable.app
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
import com.aurora.store.data.model.DownloadStatus
|
||||
import com.aurora.store.data.room.download.Download
|
||||
import com.aurora.store.data.room.update.Update
|
||||
import com.aurora.store.util.CommonUtil
|
||||
|
||||
@Composable
|
||||
fun AppUpdateItem(
|
||||
modifier: Modifier = Modifier,
|
||||
update: Update,
|
||||
download: Download? = null,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {},
|
||||
onUpdate: () -> Unit = {},
|
||||
onCancel: () -> Unit = {}
|
||||
) {
|
||||
val inProgress = download != null && download.status !in DownloadStatus.finished
|
||||
val progress = if (download?.status == DownloadStatus.DOWNLOADING) {
|
||||
download.progress.toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
var changelogExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_small)
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium))) {
|
||||
AnimatedAppIcon(
|
||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)),
|
||||
iconUrl = update.iconURL,
|
||||
inProgress = inProgress,
|
||||
progress = progress
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(dimensionResource(R.dimen.margin_small)))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = update.displayName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = update.developerName,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Text(
|
||||
text = "${CommonUtil.addSiPrefix(update.size)} • ${update.updatedOn}",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Text(
|
||||
text = "${update.versionName} (${update.versionCode})",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(dimensionResource(R.dimen.margin_small)))
|
||||
if (inProgress) {
|
||||
OutlinedButton(onClick = onCancel) {
|
||||
Text(stringResource(R.string.action_cancel))
|
||||
}
|
||||
} else {
|
||||
Button(onClick = onUpdate) {
|
||||
Text(stringResource(R.string.action_update))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { changelogExpanded = !changelogExpanded }
|
||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(Modifier.weight(1f))
|
||||
IconButton(onClick = { changelogExpanded = !changelogExpanded }) {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
if (changelogExpanded) R.drawable.ic_arrow_up else R.drawable.ic_arrow_down
|
||||
),
|
||||
contentDescription = stringResource(R.string.expand)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = changelogExpanded) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_small)
|
||||
),
|
||||
shape = RoundedCornerShape(dimensionResource(R.dimen.radius_small))
|
||||
) {
|
||||
Text(
|
||||
text = if (update.changelog.isNotEmpty()) {
|
||||
AnnotatedString.fromHtml(update.changelog)
|
||||
} else {
|
||||
AnnotatedString(stringResource(R.string.details_changelog_unavailable))
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun AppUpdateItemPreview() {
|
||||
val update = Update(
|
||||
packageName = "com.example.app",
|
||||
versionCode = 100,
|
||||
versionName = "1.0.0",
|
||||
displayName = "Example App",
|
||||
iconURL = "",
|
||||
changelog = "• Bug fixes<br>• Performance improvements",
|
||||
id = 1,
|
||||
developerName = "Developer",
|
||||
size = 5_000_000,
|
||||
updatedOn = "May 2025",
|
||||
hasValidCert = true,
|
||||
offerType = 1,
|
||||
fileList = emptyList(),
|
||||
sharedLibs = emptyList()
|
||||
)
|
||||
AppUpdateItem(update = update)
|
||||
}
|
||||
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun AppUpdateItemDownloadingPreview() {
|
||||
val update = Update(
|
||||
packageName = "com.example.app",
|
||||
versionCode = 100,
|
||||
versionName = "1.0.0",
|
||||
displayName = "Example App",
|
||||
iconURL = "",
|
||||
changelog = "",
|
||||
id = 1,
|
||||
developerName = "Developer",
|
||||
size = 5_000_000,
|
||||
updatedOn = "May 2025",
|
||||
hasValidCert = true,
|
||||
offerType = 1,
|
||||
fileList = emptyList(),
|
||||
sharedLibs = emptyList()
|
||||
)
|
||||
val download = Download.fromUpdate(update).copy(
|
||||
status = DownloadStatus.DOWNLOADING,
|
||||
progress = 45
|
||||
)
|
||||
AppUpdateItem(update = update, download = download)
|
||||
}
|
||||
@@ -71,23 +71,26 @@ fun LargeAppListItem(modifier: Modifier = Modifier, app: App, onClick: () -> Uni
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_small)
|
||||
horizontal = dimensionResource(R.dimen.padding_small),
|
||||
vertical = dimensionResource(R.dimen.padding_xsmall)
|
||||
)
|
||||
) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
|
||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium))),
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(app.iconArtwork.url)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
|
||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small))
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = dimensionResource(R.dimen.margin_small))
|
||||
) {
|
||||
Text(
|
||||
text = app.displayName,
|
||||
|
||||
Reference in New Issue
Block a user