compose: details: Show featured reviews again (review summary)
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user