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

View File

@@ -5,11 +5,21 @@
package com.aurora.store.compose.ui.details.components package com.aurora.store.compose.ui.details.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
@@ -17,18 +27,24 @@ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.dimensionResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import androidx.paging.compose.LazyPagingItems
import androidx.window.core.layout.WindowWidthSizeClass import androidx.window.core.layout.WindowWidthSizeClass
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Rating import com.aurora.gplayapi.data.models.Rating
import com.aurora.gplayapi.data.models.Review
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composables.HeaderComposable 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.RatingComposable
import com.aurora.store.compose.composables.details.ReviewComposable
import com.aurora.store.compose.composables.preview.AppPreviewProvider import com.aurora.store.compose.composables.preview.AppPreviewProvider
import java.util.Locale 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 * 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. * of the Column with proper vertical arrangement spacing in the AppDetailsScreen.
* @param rating Rating of the app * @param rating Rating of the app
* @param featuredReviews Featured app reviews
* @param onNavigateToDetailsReview Callback when the user navigates * @param onNavigateToDetailsReview Callback when the user navigates
* @param windowAdaptiveInfo Adaptive window information * @param windowAdaptiveInfo Adaptive window information
*/ */
@Composable @Composable
fun AppReviews( fun AppRatingAndReviews(
rating: Rating, rating: Rating,
featuredReviews: List<Review> = emptyList(),
onNavigateToDetailsReview: () -> Unit = {}, onNavigateToDetailsReview: () -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo() 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) @Preview(showBackground = true)
@Composable @Composable
private fun AppReviewsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) { private fun AppReviewsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))) { 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()) private val _suggestions = MutableStateFlow<List<App>>(emptyList())
val suggestions = _suggestions.asStateFlow() val suggestions = _suggestions.asStateFlow()
private val _featuredReviews = MutableStateFlow<List<Review>>(emptyList())
val featuredReviews = _featuredReviews.asStateFlow()
private val _userReview = MutableStateFlow<Review?>(null) private val _userReview = MutableStateFlow<Review?>(null)
val userReview = _userReview.asStateFlow() val userReview = _userReview.asStateFlow()
@@ -157,6 +160,7 @@ class AppDetailsViewModel @Inject constructor(
if (throwable != null) return@invokeOnCompletion if (throwable != null) return@invokeOnCompletion
fetchFavourite(packageName) fetchFavourite(packageName)
fetchFeaturedReviews(packageName)
fetchDataSafetyReport(packageName) fetchDataSafetyReport(packageName)
fetchSuggestions() fetchSuggestions()
fetchExodusPrivacyReport(packageName) 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) { fun updateTestingProgramStatus(packageName: String, subscribe: Boolean) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {

View File

@@ -65,6 +65,8 @@
<dimen name="height_nav_header">148dp</dimen> <dimen name="height_nav_header">148dp</dimen>
<dimen name="height_microg_action">96dp</dimen> <dimen name="height_microg_action">96dp</dimen>
<dimen name="review_height">200dp</dimen>
<dimen name="radius_small">8dp</dimen> <dimen name="radius_small">8dp</dimen>
<dimen name="radius_medium">10dp</dimen> <dimen name="radius_medium">10dp</dimen>
<dimen name="radius_normal">12dp</dimen> <dimen name="radius_normal">12dp</dimen>