diff --git a/app/src/main/java/com/aurora/store/compose/composables/PageIndicatorComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/PageIndicatorComposable.kt new file mode 100644 index 000000000..a631960e4 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/PageIndicatorComposable.kt @@ -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) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/details/ReviewComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/details/ReviewComposable.kt index 2420cd015..c2181580c 100644 --- a/app/src/main/java/com/aurora/store/compose/composables/details/ReviewComposable.kt +++ b/app/src/main/java/com/aurora/store/compose/composables/details/ReviewComposable.kt @@ -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 ) } } diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt index 4fe24d2d2..d4bfb272a 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt @@ -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 = 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 = 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( diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/components/AppReviews.kt b/app/src/main/java/com/aurora/store/compose/ui/details/components/AppRatingAndReviews.kt similarity index 67% rename from app/src/main/java/com/aurora/store/compose/ui/details/components/AppReviews.kt rename to app/src/main/java/com/aurora/store/compose/ui/details/components/AppRatingAndReviews.kt index d7a783108..f91d3a8f5 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/details/components/AppReviews.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/details/components/AppRatingAndReviews.kt @@ -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 = 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) } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt index 7b155813a..1f4447bbf 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt @@ -74,6 +74,9 @@ class AppDetailsViewModel @Inject constructor( private val _suggestions = MutableStateFlow>(emptyList()) val suggestions = _suggestions.asStateFlow() + private val _featuredReviews = MutableStateFlow>(emptyList()) + val featuredReviews = _featuredReviews.asStateFlow() + private val _userReview = MutableStateFlow(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 { diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 2934106ca..3bebb6e80 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -65,6 +65,8 @@ 148dp 96dp + 200dp + 8dp 10dp 12dp