compose: details: Show featured reviews again (review summary)

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-05-16 23:58:02 +08:00
parent f868a0f505
commit 0c439f3457
6 changed files with 160 additions and 5 deletions

View File

@@ -0,0 +1,82 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
/**
* Pager indicator
* Adapted from [Pager in Compose](https://developer.android.com/jetpack/compose/layouts/pager#add-page)
* @param modifier The modifier to be applied to the composable
* @param totalPages Total number of pages
* @param currentPage Currently displayed page number
*/
@Composable
fun PageIndicatorComposable(
modifier: Modifier = Modifier,
totalPages: Int,
currentPage: Int = 0,
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
dimensionResource(R.dimen.margin_xsmall),
Alignment.CenterHorizontally
)
) {
repeat(totalPages) { iteration ->
val isSelected = currentPage == iteration
val color by animateColorAsState(
targetValue = if (isSelected) {
Color.DarkGray
} else {
Color.LightGray
},
animationSpec = tween()
)
val size by animateDpAsState(
targetValue = if (isSelected) {
dimensionResource(R.dimen.radius_normal)
} else {
dimensionResource(R.dimen.radius_small)
},
animationSpec = tween()
)
Box(
modifier = modifier
.size(size)
.clip(CircleShape)
.background(color = color)
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun PageIndicatorComposablePreview() {
PageIndicatorComposable(totalPages = 5)
}

View File

@@ -88,7 +88,8 @@ fun ReviewComposable(modifier: Modifier = Modifier, review: Review) {
)
Text(
text = review.comment,
style = MaterialTheme.typography.bodySmall
style = MaterialTheme.typography.bodySmall,
overflow = TextOverflow.Ellipsis
)
}
}

View File

@@ -45,6 +45,7 @@ import com.aurora.extensions.browse
import com.aurora.extensions.share
import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Review
import com.aurora.store.R
import com.aurora.store.compose.composables.HeaderComposable
import com.aurora.store.compose.composables.TopAppBarComposable
@@ -62,7 +63,7 @@ import com.aurora.store.compose.ui.details.components.AppDataSafety
import com.aurora.store.compose.ui.details.components.AppDetails
import com.aurora.store.compose.ui.details.components.AppDeveloperDetails
import com.aurora.store.compose.ui.details.components.AppPrivacy
import com.aurora.store.compose.ui.details.components.AppReviews
import com.aurora.store.compose.ui.details.components.AppRatingAndReviews
import com.aurora.store.compose.ui.details.components.AppScreenshots
import com.aurora.store.compose.ui.details.components.AppTags
import com.aurora.store.compose.ui.details.components.AppTesting
@@ -89,6 +90,7 @@ fun AppDetailsScreen(
val context = LocalContext.current
val app by viewModel.app.collectAsStateWithLifecycle()
val featuredReviews by viewModel.featuredReviews.collectAsStateWithLifecycle()
val favorite by viewModel.favourite.collectAsStateWithLifecycle()
val exodusReport by viewModel.exodusReport.collectAsStateWithLifecycle()
val dataSafetyReport by viewModel.dataSafetyReport.collectAsStateWithLifecycle()
@@ -132,6 +134,7 @@ fun AppDetailsScreen(
} else {
ScreenContentApp(
app = this,
featuredReviews = featuredReviews,
isFavorite = favorite,
isAnonymous = viewModel.authProvider.isAnonymous,
download = download,
@@ -204,6 +207,7 @@ private fun ScreenContentError(onNavigateUp: () -> Unit = {}) {
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun ScreenContentApp(
app: App,
featuredReviews: List<Review> = emptyList(),
isFavorite: Boolean = false,
isAnonymous: Boolean = true,
download: Download? = null,
@@ -262,6 +266,7 @@ private fun ScreenContentApp(
AnimatedPane {
ScreenContentAppMainPane(
app = app,
featuredReviews = featuredReviews,
download = download,
installProgress = installProgress,
isAnonymous = isAnonymous,
@@ -329,6 +334,7 @@ private fun ScreenContentApp(
@Composable
private fun ScreenContentAppMainPane(
app: App,
featuredReviews: List<Review> = emptyList(),
download: Download?,
installProgress: Float?,
isAnonymous: Boolean,
@@ -443,7 +449,11 @@ private fun ScreenContentAppMainPane(
onNavigateToScreenshot = onNavigateToDetailsScreenshot
)
AppReviews(rating = app.rating, onNavigateToDetailsReview = onNavigateToDetailsReview)
AppRatingAndReviews(
rating = app.rating,
featuredReviews = featuredReviews,
onNavigateToDetailsReview = onNavigateToDetailsReview
)
if (!isAnonymous && app.testingProgram?.isAvailable == true) {
AppTesting(

View File

@@ -5,11 +5,21 @@
package com.aurora.store.compose.ui.details.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
@@ -17,18 +27,24 @@ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
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.graphics.Color
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.util.fastForEach
import androidx.paging.compose.LazyPagingItems
import androidx.window.core.layout.WindowWidthSizeClass
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Rating
import com.aurora.gplayapi.data.models.Review
import com.aurora.store.R
import com.aurora.store.compose.composables.HeaderComposable
import com.aurora.store.compose.composables.PageIndicatorComposable
import com.aurora.store.compose.composables.details.RatingComposable
import com.aurora.store.compose.composables.details.ReviewComposable
import com.aurora.store.compose.composables.preview.AppPreviewProvider
import java.util.Locale
@@ -36,12 +52,14 @@ import java.util.Locale
* Composable to display reviews of the app, supposed to be used as a part
* of the Column with proper vertical arrangement spacing in the AppDetailsScreen.
* @param rating Rating of the app
* @param featuredReviews Featured app reviews
* @param onNavigateToDetailsReview Callback when the user navigates
* @param windowAdaptiveInfo Adaptive window information
*/
@Composable
fun AppReviews(
fun AppRatingAndReviews(
rating: Rating,
featuredReviews: List<Review> = emptyList(),
onNavigateToDetailsReview: () -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
@@ -98,12 +116,38 @@ fun AppReviews(
}
}
}
if (featuredReviews.isNotEmpty()) {
val pagerState = rememberPagerState { featuredReviews.size }
HorizontalPager(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_medium)),
state = pagerState,
pageSpacing = dimensionResource(R.dimen.margin_normal)
) { page ->
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small)))
.background(color = MaterialTheme.colorScheme.secondaryContainer)
.requiredHeight(dimensionResource(R.dimen.review_height))
) {
ReviewComposable(review = featuredReviews[page])
}
}
PageIndicatorComposable(
totalPages = featuredReviews.size,
currentPage = pagerState.currentPage
)
}
}
@Preview(showBackground = true)
@Composable
private fun AppReviewsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))) {
AppReviews(rating = app.rating)
AppRatingAndReviews(rating = app.rating)
}
}

View File

@@ -74,6 +74,9 @@ class AppDetailsViewModel @Inject constructor(
private val _suggestions = MutableStateFlow<List<App>>(emptyList())
val suggestions = _suggestions.asStateFlow()
private val _featuredReviews = MutableStateFlow<List<Review>>(emptyList())
val featuredReviews = _featuredReviews.asStateFlow()
private val _userReview = MutableStateFlow<Review?>(null)
val userReview = _userReview.asStateFlow()
@@ -157,6 +160,7 @@ class AppDetailsViewModel @Inject constructor(
if (throwable != null) return@invokeOnCompletion
fetchFavourite(packageName)
fetchFeaturedReviews(packageName)
fetchDataSafetyReport(packageName)
fetchSuggestions()
fetchExodusPrivacyReport(packageName)
@@ -166,6 +170,18 @@ class AppDetailsViewModel @Inject constructor(
}
}
private fun fetchFeaturedReviews(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
_featuredReviews.value = reviewsHelper.getReviewSummary(packageName)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch featured app reviews", exception)
_featuredReviews.value = emptyList()
}
}
}
fun updateTestingProgramStatus(packageName: String, subscribe: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
try {