compose: Initial migration of AppDetails* logic to compose [2/*]

Deprecate setting to hide similar and related apps as they will be always
listed in the suggestions pane on widescreen devices

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-04-28 14:16:26 +08:00
parent f6f858e9f5
commit 097f5a7375
111 changed files with 1108 additions and 2571 deletions

View File

@@ -32,6 +32,7 @@ import com.aurora.store.compose.composables.preview.AppPreviewProvider
/**
* Composable to show some information
* @param modifier Modifier to change the composable
* @param title Title of the information
* @param description Information to show
* @param icon Optional icon representing the information
@@ -39,13 +40,14 @@ import com.aurora.store.compose.composables.preview.AppPreviewProvider
*/
@Composable
fun InfoComposable(
modifier: Modifier = Modifier,
title: AnnotatedString,
description: AnnotatedString? = null,
@DrawableRes icon: Int? = null,
onClick: (() -> Unit)? = null
) {
Row(
modifier = Modifier
modifier = modifier
.fillMaxWidth()
.clickable(onClick = { if (onClick != null) onClick() }, enabled = onClick != null)
.padding(

View File

@@ -14,6 +14,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
@@ -21,6 +22,7 @@ import com.aurora.store.R
/**
* A top app bar composable to be used with Scaffold in different Screen
* @param title Title of the screen
* @param navigationIcon Icon for the navigation button
* @param onNavigateUp Action when user clicks the navigation icon
* @param actions Actions to display on the top app bar (for e.g. menu)
*/
@@ -28,17 +30,17 @@ import com.aurora.store.R
@OptIn(ExperimentalMaterial3Api::class)
fun TopAppBarComposable(
title: String? = null,
onNavigateUp: () -> Unit,
navigationIcon: ImageVector = Icons.AutoMirrored.Filled.ArrowBack,
onNavigateUp: (() -> Unit)? = null,
actions: @Composable (RowScope.() -> Unit) = {}
) {
TopAppBar(
title = { if (title != null) Text(text = title) },
navigationIcon = {
IconButton(onClick = onNavigateUp) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null
)
if (onNavigateUp != null) {
IconButton(onClick = onNavigateUp) {
Icon(imageVector = navigationIcon, contentDescription = null)
}
}
},
actions = actions

View File

@@ -0,0 +1,119 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables.app
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
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.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.AsyncImage
import coil3.compose.LocalAsyncImagePreviewHandler
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composables.preview.AppPreviewProvider
import com.aurora.store.compose.composables.preview.coilPreviewProvider
/**
* Composable to show icon for an app that can be animated to also show install progress
* @param modifier Modifier to alter the composable
* @param iconUrl URL of the app icon
* @param progress Progress to show, for e.g. download or install
* @param inProgress Whether to show indeterminate or determinate progress bar
*/
@Composable
fun AnimatedAppIconComposable(
modifier: Modifier = Modifier,
iconUrl: String,
progress: Float = 0F,
inProgress: Boolean = false
) {
val animatedScale by animateFloatAsState(
targetValue = if (inProgress) 0.75F else 1F,
animationSpec = tween(durationMillis = 300)
)
val clip = when {
inProgress -> CircleShape
else -> RoundedCornerShape(dimensionResource(R.dimen.radius_medium))
}
Box(modifier = modifier, contentAlignment = Alignment.Center) {
if (inProgress) {
val indicatorModifier = Modifier.fillMaxSize()
if (progress > 0) {
CircularProgressIndicator(
modifier = indicatorModifier,
progress = { progress / 100 }
)
} else {
CircularProgressIndicator(modifier = indicatorModifier)
}
}
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(iconUrl)
.crossfade(true)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.graphicsLayer(scaleX = animatedScale, scaleY = animatedScale)
.clip(clip)
)
}
}
private class ProgressProvider: PreviewParameterProvider<Float> {
override val values: Sequence<Float>
get() = sequenceOf(0F, 50F)
}
@Preview(showBackground = true)
@Composable
@OptIn(ExperimentalCoilApi::class)
private fun AnimatedAppIconPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) {
AnimatedAppIconComposable(
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
iconUrl = app.iconArtwork.url
)
}
}
@Preview(showBackground = true)
@Composable
@OptIn(ExperimentalCoilApi::class)
private fun AnimatedAppIconPreview(@PreviewParameter(ProgressProvider::class) progress: Float) {
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) {
AnimatedAppIconComposable(
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
iconUrl = "",
inProgress = true,
progress = progress
)
}
}

View File

@@ -1,47 +0,0 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables.app
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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 com.aurora.store.R
/**
* Composable to show error message when no apps are available for a request
* @param message Message for error
* @see NoAppAltComposable
*/
@Composable
fun NoAppAltComposable(@StringRes message: Int) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_xsmall))
) {
Text(
text = stringResource(message),
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@Preview(showBackground = true)
@Composable
private fun NoAppAltComposablePreview() {
NoAppAltComposable(message = R.string.details_no_dependencies)
}

View File

@@ -31,7 +31,6 @@ import com.aurora.store.R
* @param message Message for error
* @param actionMessage Message to show on action button; defaults to null with button not visible
* @param onAction Callback when action button is clicked
* @see NoAppAltComposable
*/
@Composable
fun NoAppComposable(

View File

@@ -11,13 +11,9 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import com.aurora.extensions.parentViewModel
import com.aurora.store.compose.ui.commons.BlacklistScreen
import com.aurora.store.compose.ui.details.AppDetailsScreen
import com.aurora.store.compose.ui.details.DetailsExodusScreen
import com.aurora.store.compose.ui.details.DetailsMoreScreen
import com.aurora.store.compose.ui.details.DetailsReviewScreen
import com.aurora.store.compose.ui.details.DetailsScreenshotScreen
import com.aurora.store.compose.ui.dev.DevProfileScreen
/**
* Navigation graph for compose screens
@@ -45,46 +41,21 @@ fun NavGraph(navHostController: NavHostController, startDestination: Screen) {
val appDetails = backstackEntry.toRoute<Screen.AppDetails>()
AppDetailsScreen(
packageName = appDetails.packageName,
viewModel = backstackEntry.parentViewModel(navHostController),
onNavigateUp = { onNavigateUp() },
onNavigateToDetailsMore = { navHostController.navigate(Screen.DetailsMore) },
onNavigateToDetailsReview = { navHostController.navigate(Screen.DetailsReview) },
onNavigateToDetailsExodus = { navHostController.navigate(Screen.DetailsExodus) },
onNavigateToDetailsScreenshot = { index ->
navHostController.navigate(
Screen.DetailsScreenshot(index)
)
onNavigateToAppDetails = { packageName ->
navHostController.navigate(Screen.AppDetails(packageName))
}
)
}
composable<Screen.DetailsExodus> { backstackEntry ->
DetailsExodusScreen(
composable<Screen.DevProfile> { backstackEntry ->
val devProfile = backstackEntry.toRoute<Screen.DevProfile>()
DevProfileScreen(
developerId = devProfile.developerId,
onNavigateUp = { onNavigateUp() },
viewModel = backstackEntry.parentViewModel(navHostController)
)
}
composable<Screen.DetailsMore> { backstackEntry ->
DetailsMoreScreen(
onNavigateUp = { onNavigateUp() },
viewModel = backstackEntry.parentViewModel(navHostController)
)
}
composable<Screen.DetailsScreenshot> { backstackEntry ->
val screenshotDetails = backstackEntry.toRoute<Screen.DetailsScreenshot>()
DetailsScreenshotScreen(
index = screenshotDetails.index,
onNavigateUp = { onNavigateUp() },
viewModel = backstackEntry.parentViewModel(navHostController)
)
}
composable<Screen.DetailsReview> { backstackEntry ->
DetailsReviewScreen(
onNavigateUp = { onNavigateUp() },
viewModel = backstackEntry.parentViewModel(navHostController)
onNavigateToAppDetails = { packageName ->
navHostController.navigate(Screen.AppDetails(packageName))
}
)
}
}

View File

@@ -31,18 +31,33 @@ sealed class Screen(
@Serializable
data object Blacklist : Screen(label = R.string.title_blacklist_manager)
@Serializable
data class DevProfile(val developerId: String): Screen()
@Serializable
data class AppDetails(val packageName: String) : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data object DetailsMore : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data class DetailsScreenshot(val index: Int) : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data object DetailsExodus : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data object DetailsReview : Screen()
}

View File

@@ -18,6 +18,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
@@ -25,36 +28,44 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.NavigableSupportingPaneScaffold
import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.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.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.util.fastForEach
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.core.layout.WindowWidthSizeClass
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.AsyncImage
import coil3.compose.LocalAsyncImagePreviewHandler
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.aurora.extensions.bodyVerySmall
import com.aurora.extensions.browse
import com.aurora.extensions.copyToClipBoard
@@ -68,6 +79,8 @@ import com.aurora.store.R
import com.aurora.store.compose.composables.HeaderComposable
import com.aurora.store.compose.composables.InfoComposable
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.compose.composables.app.AnimatedAppIconComposable
import com.aurora.store.compose.composables.app.AppListComposable
import com.aurora.store.compose.composables.app.AppProgressComposable
import com.aurora.store.compose.composables.app.AppTagComposable
import com.aurora.store.compose.composables.app.NoAppComposable
@@ -75,6 +88,8 @@ import com.aurora.store.compose.composables.details.RatingComposable
import com.aurora.store.compose.composables.details.ScreenshotComposable
import com.aurora.store.compose.composables.preview.AppPreviewProvider
import com.aurora.store.compose.composables.preview.coilPreviewProvider
import com.aurora.store.compose.navigation.Screen
import com.aurora.store.compose.ui.dev.DevProfileScreen
import com.aurora.store.compose.ui.dialogs.ManualDownloadDialog
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.Report
@@ -84,17 +99,16 @@ import com.aurora.store.util.CommonUtil
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.PackageUtil.PACKAGE_NAME_GMS
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import kotlinx.coroutines.launch
import java.util.Locale
import kotlin.random.Random
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
@Composable
fun AppDetailsScreen(
packageName: String,
onNavigateUp: () -> Unit,
onNavigateToDetailsMore: () -> Unit,
onNavigateToDetailsScreenshot: (index: Int) -> Unit,
onNavigateToDetailsReview: () -> Unit,
onNavigateToDetailsExodus: () -> Unit,
onNavigateToAppDetails: (packageName: String) -> Unit,
viewModel: AppDetailsViewModel = hiltViewModel()
) {
val context = LocalContext.current
@@ -104,17 +118,23 @@ fun AppDetailsScreen(
val dataSafetyReport by viewModel.dataSafetyReport.collectAsStateWithLifecycle()
val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle()
val download by viewModel.download.collectAsStateWithLifecycle()
val installProgress by viewModel.installProgress.collectAsStateWithLifecycle()
val suggestions by viewModel.suggestions.collectAsStateWithLifecycle()
var shouldShowManualDownloadDialog by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(key1 = Unit) { viewModel.fetchAppDetails(packageName) }
LaunchedEffect(key1 = Unit) {
viewModel.purchaseStatus.collect { success ->
if (success) {
shouldShowManualDownloadDialog = false
context.toast(R.string.toast_manual_available)
if (shouldShowManualDownloadDialog) {
if (success) {
shouldShowManualDownloadDialog = false
context.toast(R.string.toast_manual_available)
} else {
context.toast(R.string.toast_manual_unavailable)
}
} else {
context.toast(R.string.toast_manual_unavailable)
context.toast(R.string.download_failed)
}
}
}
@@ -135,19 +155,18 @@ fun AppDetailsScreen(
if (this.packageName.isBlank()) {
ScreenContentLoading(onNavigateUp = onNavigateUp)
} else {
ScreenContent(
ScreenContentApp(
app = this,
isAnonymous = viewModel.authProvider.isAnonymous,
suggestions = suggestions,
download = download,
installProgress = installProgress,
plexusScores = plexusScores,
dataSafetyReport = dataSafetyReport,
exodusReport = exodusReport,
hasValidUpdate = viewModel.hasValidUpdate,
showSimilarApps = viewModel.showSimilarApps,
onNavigateUp = onNavigateUp,
onNavigateToDetailsMore = onNavigateToDetailsMore,
onNavigateToDetailsScreenshot = onNavigateToDetailsScreenshot,
onNavigateToDetailsReview = onNavigateToDetailsReview,
onNavigateToDetailsExodus = onNavigateToDetailsExodus,
onNavigateToAppDetails = onNavigateToAppDetails,
onDownload = { viewModel.download(this) },
onManualDownload = { shouldShowManualDownloadDialog = true },
onCancelDownload = { viewModel.cancelDownload(this) },
@@ -160,6 +179,9 @@ fun AppDetailsScreen(
} catch (exception: ActivityNotFoundException) {
context.toast(context.getString(R.string.unable_to_open))
}
},
onTestingSubscriptionChange = { subscribe ->
viewModel.updateTestingProgramStatus(packageName, subscribe)
}
)
}
@@ -171,6 +193,9 @@ fun AppDetailsScreen(
}
}
/**
* Composable to show progress while fetching app details
*/
@Composable
private fun ScreenContentLoading(onNavigateUp: () -> Unit = {}) {
Scaffold(
@@ -180,6 +205,9 @@ private fun ScreenContentLoading(onNavigateUp: () -> Unit = {}) {
}
}
/**
* Composable to display errors related to fetching app details
*/
@Composable
private fun ScreenContentError(onNavigateUp: () -> Unit = {}) {
Scaffold(
@@ -193,28 +221,186 @@ private fun ScreenContentError(onNavigateUp: () -> Unit = {}) {
}
}
/**
* Composable to display app details and suggestions
*/
@Composable
private fun ScreenContent(
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun ScreenContentApp(
app: App,
isAnonymous: Boolean = true,
suggestions: List<App> = emptyList(),
download: Download? = null,
installProgress: Float? = null,
plexusScores: Scores? = null,
dataSafetyReport: DataSafetyReport? = null,
exodusReport: Report? = null,
hasValidUpdate: Boolean = false,
showSimilarApps: Boolean = false,
onNavigateUp: () -> Unit = {},
onNavigateToDetailsMore: () -> Unit = {},
onNavigateToDetailsScreenshot: (index: Int) -> Unit = {},
onNavigateToDetailsReview: () -> Unit = {},
onNavigateToDetailsExodus: () -> Unit = {},
onNavigateToAppDetails: (packageName: String) -> Unit = {},
onDownload: () -> Unit = {},
onManualDownload: () -> Unit = {},
onCancelDownload: () -> Unit = {},
onUninstall: () -> Unit = {},
onOpen: () -> Unit = {}
onOpen: () -> Unit = {},
onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {}
) {
val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator<Screen>()
val coroutineScope = rememberCoroutineScope()
fun showMainPane() {
coroutineScope.launch {
scaffoldNavigator.navigateBack()
}
}
fun showExtraPane(screen: Screen) {
coroutineScope.launch {
scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Extra, screen)
}
}
NavigableSupportingPaneScaffold(
navigator = scaffoldNavigator,
mainPane = {
AnimatedPane {
ScreenContentAppMainPane(
app = app,
isAnonymous = isAnonymous,
download = download,
installProgress = installProgress,
plexusScores = plexusScores,
dataSafetyReport = dataSafetyReport,
exodusReport = exodusReport,
hasValidUpdate = hasValidUpdate,
onNavigateUp = onNavigateUp,
onDownload = onDownload,
onManualDownload = onManualDownload,
onCancelDownload = onCancelDownload,
onUninstall = onUninstall,
onOpen = onOpen,
onNavigateToDetailsDevProfile = { showExtraPane(Screen.DevProfile(it)) },
onNavigateToDetailsMore = { showExtraPane(Screen.DetailsMore) },
onNavigateToDetailsReview = { showExtraPane(Screen.DetailsReview) },
onNavigateToDetailsExodus = { showExtraPane(Screen.DetailsExodus) },
onNavigateToDetailsScreenshot = { showExtraPane(Screen.DetailsScreenshot(it)) },
onTestingSubscriptionChange = onTestingSubscriptionChange
)
}
},
supportingPane = {
AnimatedPane(modifier = Modifier.safeContentPadding()) {
ScreenContentAppSupportingPane(
suggestions = suggestions,
onNavigateToAppDetails = onNavigateToAppDetails
)
}
},
extraPane = {
scaffoldNavigator.currentDestination?.contentKey?.let { screen ->
AnimatedPane {
when (screen) {
is Screen.DetailsReview -> DetailsReviewScreen(onNavigateUp = ::showMainPane)
is Screen.DetailsExodus -> DetailsExodusScreen(onNavigateUp = ::showMainPane)
is Screen.DetailsMore -> DetailsMoreScreen(
onNavigateUp = ::showMainPane,
onNavigateToAppDetails = onNavigateToAppDetails
)
is Screen.DetailsScreenshot -> DetailsScreenshotScreen(
index = screen.index,
onNavigateUp = ::showMainPane
)
// TODO: Pass the real developerId
is Screen.DevProfile -> DevProfileScreen(
developerId = app.developerName,
onNavigateUp = ::showMainPane,
onNavigateToAppDetails = { onNavigateToAppDetails(it) }
)
else -> {}
}
}
}
}
)
}
/**
* Composable to display app details
*/
@Composable
private fun ScreenContentAppMainPane(
app: App,
download: Download?,
installProgress: Float?,
isAnonymous: Boolean,
plexusScores: Scores?,
dataSafetyReport: DataSafetyReport?,
exodusReport: Report?,
hasValidUpdate: Boolean,
onNavigateUp: () -> Unit,
onNavigateToDetailsDevProfile: (developerName: String) -> Unit,
onNavigateToDetailsMore: () -> Unit,
onNavigateToDetailsScreenshot: (index: Int) -> Unit,
onNavigateToDetailsReview: () -> Unit,
onNavigateToDetailsExodus: () -> Unit,
onDownload: () -> Unit,
onManualDownload: () -> Unit,
onCancelDownload: () -> Unit,
onUninstall: () -> Unit,
onOpen: () -> Unit,
onTestingSubscriptionChange: (subscribe: Boolean) -> Unit
) {
val context = LocalContext.current
@Composable
fun SetupAppActions() {
when {
download?.isRunning == true -> {
AppActions(
primaryActionDisplayName = stringResource(R.string.action_open),
secondaryActionDisplayName = stringResource(R.string.action_cancel),
isPrimaryActionEnabled = false,
onSecondaryAction = onCancelDownload
)
}
app.isInstalled && hasValidUpdate -> {
AppActions(
primaryActionDisplayName = stringResource(R.string.action_update),
secondaryActionDisplayName = stringResource(R.string.action_uninstall),
onPrimaryAction = onDownload,
onSecondaryAction = onUninstall
)
}
app.isInstalled -> {
AppActions(
primaryActionDisplayName = stringResource(R.string.action_open),
secondaryActionDisplayName = stringResource(R.string.action_uninstall),
onPrimaryAction = onOpen,
onSecondaryAction = onUninstall
)
}
else -> {
val primaryActionName = if (PackageUtil.isArchived(context, app.packageName)) {
stringResource(R.string.action_unarchive)
} else {
if (app.isFree) stringResource(R.string.action_install) else app.price
}
AppActions(
primaryActionDisplayName = primaryActionName,
secondaryActionDisplayName = stringResource(R.string.title_manual_download),
onPrimaryAction = onDownload,
onSecondaryAction = onManualDownload
)
}
}
}
Scaffold(
topBar = {
TopAppBarComposable(
@@ -230,53 +416,24 @@ private fun ScreenContent(
.padding(dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) {
// TODO: Deal with download status
AppDetails(app = app, hasValidUpdate = hasValidUpdate)
when {
download?.isRunning == true -> {
AppActions(
primaryActionDisplayName = stringResource(R.string.action_open),
secondaryActionDisplayName = stringResource(R.string.action_cancel),
isPrimaryActionEnabled = false,
onSecondaryAction = onCancelDownload
)
}
app.isInstalled && hasValidUpdate -> {
AppActions(
primaryActionDisplayName = stringResource(R.string.action_update),
secondaryActionDisplayName = stringResource(R.string.action_uninstall),
onPrimaryAction = onDownload,
onSecondaryAction = onUninstall
)
}
app.isInstalled -> {
AppActions(
primaryActionDisplayName = stringResource(R.string.action_open),
secondaryActionDisplayName = stringResource(R.string.action_uninstall),
onPrimaryAction = onOpen,
onSecondaryAction = onUninstall
)
}
else -> {
val primaryActionName = if (PackageUtil.isArchived(context, app.packageName)) {
stringResource(R.string.action_unarchive)
} else {
if (app.isFree) stringResource(R.string.action_install) else app.price
}
AppActions(
primaryActionDisplayName = primaryActionName,
secondaryActionDisplayName = stringResource(R.string.title_manual_download),
onPrimaryAction = onDownload,
onSecondaryAction = onManualDownload
)
}
val isDownloading = download != null && download.isRunning
val isInstalling = installProgress != null
val progress = when {
isDownloading -> download?.progress?.toFloat() ?: 0F
isInstalling -> installProgress ?: 0F
else -> 0F
}
AppDetails(
app = app,
inProgress = isDownloading || isInstalling,
progress = progress,
onNavigateToDetailsDevApps = onNavigateToDetailsDevProfile,
hasValidUpdate = hasValidUpdate
)
SetupAppActions()
AppTags(app = app)
AppChangelog(changelog = app.changes)
HeaderComposable(
@@ -292,6 +449,13 @@ private fun ScreenContent(
AppReviews(rating = app.rating, onNavigateToDetailsReview = onNavigateToDetailsReview)
if (!isAnonymous && app.testingProgram?.isAvailable == true) {
AppTesting(
isSubscribed = app.testingProgram!!.isSubscribed,
onTestingSubscriptionChange = onTestingSubscriptionChange
)
}
AppCompatibility(
needsGms = app.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS),
plexusScores = plexusScores
@@ -322,24 +486,67 @@ private fun ScreenContent(
}
}
/**
* Composable to display similar and related app suggestions
*/
@Composable
private fun ScreenContentAppSupportingPane(
suggestions: List<App> = emptyList(),
onNavigateToAppDetails: (packageName: String) -> Unit = {}
) {
Scaffold(
topBar = { TopAppBarComposable() }
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Row(
modifier = Modifier.padding(dimensionResource(R.dimen.margin_medium)),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.ic_suggestions),
contentDescription = null
)
HeaderComposable(title = stringResource(R.string.pref_ui_similar_apps))
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(vertical = dimensionResource(R.dimen.padding_medium))
) {
items(items = suggestions, key = { item -> item.id }) { app ->
AppListComposable(
app = app,
onClick = { onNavigateToAppDetails(app.packageName) }
)
}
}
}
}
}
/**
* Composable to display basic app details
*/
@Composable
private fun AppDetails(app: App, hasValidUpdate: Boolean = false) {
private fun AppDetails(
app: App,
progress: Float = 0F,
inProgress: Boolean = false,
onNavigateToDetailsDevApps: (developerName: String) -> Unit,
hasValidUpdate: Boolean = false,
) {
val context = LocalContext.current
Row(modifier = Modifier.fillMaxWidth()) {
AsyncImage(
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_large))
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
AnimatedAppIconComposable(
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
iconUrl = app.iconArtwork.url,
inProgress = inProgress,
progress = progress
)
Column(modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small))) {
Text(
@@ -349,6 +556,8 @@ private fun AppDetails(app: App, hasValidUpdate: Boolean = false) {
overflow = TextOverflow.Ellipsis
)
Text(
modifier = Modifier
.clickable(onClick = { onNavigateToDetailsDevApps(app.developerName) }),
text = app.developerName,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
@@ -383,14 +592,20 @@ private fun AppActions(
isPrimaryActionEnabled: Boolean = true,
isSecondaryActionEnabled: Boolean = true,
onPrimaryAction: () -> Unit = {},
onSecondaryAction: () -> Unit = {}
onSecondaryAction: () -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
) {
val buttonWidthModifier = when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> Modifier.weight(1F)
else -> Modifier.widthIn(min = dimensionResource(R.dimen.width_button))
}
FilledTonalButton(
modifier = Modifier.weight(1F),
modifier = buttonWidthModifier,
onClick = onSecondaryAction,
enabled = isSecondaryActionEnabled
) {
@@ -402,7 +617,7 @@ private fun AppActions(
}
Button(
modifier = Modifier.weight(1F),
modifier = buttonWidthModifier,
onClick = onPrimaryAction,
enabled = isPrimaryActionEnabled
) {
@@ -551,7 +766,6 @@ private fun AppReviews(rating: Rating, onNavigateToDetailsReview: () -> Unit) {
HeaderComposable(
title = stringResource(R.string.details_ratings),
subtitle = stringResource(R.string.details_ratings_subtitle),
onClick = onNavigateToDetailsReview
)
@@ -719,12 +933,57 @@ private fun AppPrivacy(report: Report?, onNavigateToDetailsExodus: (() -> Unit)?
)
}
@Preview
/**
* Composable to display app's beta testing status
*/
@Composable
private fun AppTesting(
isSubscribed: Boolean = false,
onTestingSubscriptionChange: (subscribe: Boolean) -> Unit
) {
HeaderComposable(title = stringResource(R.string.details_beta))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
) {
InfoComposable(
modifier = Modifier.weight(1F),
icon = R.drawable.ic_experiment,
title = AnnotatedString(
text = if (isSubscribed) {
stringResource(R.string.details_beta_subscribed)
} else {
stringResource(R.string.details_beta_available)
}
),
description = AnnotatedString(text = stringResource(R.string.details_beta_description))
)
FilledTonalButton(onClick = { onTestingSubscriptionChange(!isSubscribed) }) {
Text(
text = if (isSubscribed) {
stringResource(R.string.action_leave)
} else {
stringResource(R.string.action_join)
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
@PreviewScreenSizes
@Composable
@OptIn(ExperimentalCoilApi::class)
private fun AppDetailsScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) {
ScreenContent(app = app, hasValidUpdate = false)
ScreenContentApp(
app = app,
isAnonymous = false,
suggestions = List(5) { app.copy(id = Random.nextInt()) },
hasValidUpdate = false
)
}
}

View File

@@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@@ -20,7 +22,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.core.layout.WindowWidthSizeClass
import com.aurora.Constants.EXODUS_REPORT_URL
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.extensions.browse
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
@@ -30,20 +34,31 @@ import com.aurora.store.compose.composables.details.ExodusComposable
import com.aurora.store.compose.composables.preview.AppPreviewProvider
import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.aurora.store.viewmodel.details.DetailsExodusViewModel
@Composable
fun DetailsExodusScreen(
onNavigateUp: () -> Unit,
viewModel: AppDetailsViewModel = hiltViewModel()
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(),
detailsExodusViewModel: DetailsExodusViewModel = hiltViewModel { factory: DetailsExodusViewModel.Factory ->
factory.create(appDetailsViewModel.exodusReport.value!!)
},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val app by viewModel.app.collectAsStateWithLifecycle()
val exodusReport by viewModel.exodusReport.collectAsStateWithLifecycle()
val app by appDetailsViewModel.app.collectAsStateWithLifecycle()
val exodusReport by appDetailsViewModel.exodusReport.collectAsStateWithLifecycle()
val trackers by detailsExodusViewModel.trackers.collectAsStateWithLifecycle()
val topAppBarTitle = when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> app!!.displayName
else -> stringResource(R.string.details_privacy)
}
ScreenContent(
topAppBarTitle = app!!.displayName,
topAppBarTitle = topAppBarTitle,
id = exodusReport!!.id,
version = exodusReport!!.version,
trackers = viewModel.getExodusTrackersFromReport(),
trackers = trackers,
onNavigateUp = onNavigateUp,
)
}
@@ -55,12 +70,17 @@ private fun ScreenContent(
version: String = String(),
trackers: List<ExodusTracker> = emptyList(),
onNavigateUp: () -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val context = LocalContext.current
Scaffold(
topBar = {
TopAppBarComposable(title = topAppBarTitle, onNavigateUp = onNavigateUp)
TopAppBarComposable(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
}
) { paddingValues ->
LazyColumn(

View File

@@ -8,12 +8,17 @@ package com.aurora.store.compose.ui.details
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@@ -26,32 +31,63 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.core.layout.WindowWidthSizeClass
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.LocalAsyncImagePreviewHandler
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composables.HeaderComposable
import com.aurora.store.compose.composables.InfoComposable
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.compose.composables.app.AppComposable
import com.aurora.store.compose.composables.preview.AppPreviewProvider
import com.aurora.store.compose.composables.preview.coilPreviewProvider
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.aurora.store.viewmodel.details.DetailsMoreViewModel
import java.util.Locale
@Composable
fun DetailsMoreScreen(
onNavigateUp: () -> Unit,
viewModel: AppDetailsViewModel = hiltViewModel()
onNavigateToAppDetails: (packageName: String) -> Unit,
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(),
detailsMoreViewModel: DetailsMoreViewModel = hiltViewModel { factory: DetailsMoreViewModel.Factory ->
factory.create(appDetailsViewModel.app.value!!.dependencies.dependentPackages)
}
) {
val app by viewModel.app.collectAsStateWithLifecycle()
val app by appDetailsViewModel.app.collectAsStateWithLifecycle()
val dependencies by detailsMoreViewModel.dependentApps.collectAsStateWithLifecycle()
ScreenContent(app = app!!, onNavigateUp = onNavigateUp)
ScreenContent(
app = app!!,
dependencies = dependencies,
onNavigateUp = onNavigateUp,
onNavigateToAppDetails = onNavigateToAppDetails
)
}
@Composable
private fun ScreenContent(app: App, onNavigateUp: () -> Unit = {}) {
private fun ScreenContent(
app: App,
dependencies: List<App>? = null,
onNavigateUp: () -> Unit = {},
onNavigateToAppDetails: (packageName: String) -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val topAppBarTitle = when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> app.displayName
else -> stringResource(R.string.details_more_about_app)
}
Scaffold(
topBar = { TopAppBarComposable(title = app.displayName, onNavigateUp = onNavigateUp) }
topBar = {
TopAppBarComposable(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
}
) { paddingValues ->
Column(
modifier = Modifier
@@ -69,51 +105,88 @@ private fun ScreenContent(app: App, onNavigateUp: () -> Unit = {}) {
style = MaterialTheme.typography.bodyMedium
)
if (app.appInfo.appInfoMap.isNotEmpty()) {
HeaderComposable(title = stringResource(R.string.details_more_info))
InfoComposable(
title = AnnotatedString(
text = stringResource(R.string.details_more_package_name)
),
description = AnnotatedString(text = app.packageName)
if (dependencies != null) {
AppDependencies(
dependencies = dependencies,
onNavigateToAppDetails = onNavigateToAppDetails
)
}
InfoComposable(
title = AnnotatedString(
text = stringResource(R.string.details_more_content_rating)
),
description = AnnotatedString(text = app.contentRating.title)
)
AppInfoMore(app = app)
}
}
}
app.appInfo.appInfoMap.forEach { (title, subtitle) ->
InfoComposable(
title = AnnotatedString(
text = title.replace("_", " ")
.lowercase(Locale.getDefault())
.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
),
description = AnnotatedString(text = subtitle)
)
}
InfoComposable(
title = AnnotatedString(
text = stringResource(R.string.details_more_target_api)
),
description = AnnotatedString(text = "API ${app.targetSdk}")
/**
* Composable to show dependencies of an app
*/
@Composable
private fun AppDependencies(
dependencies: List<App>,
onNavigateToAppDetails: (packageName: String) -> Unit
) {
HeaderComposable(title = stringResource(R.string.details_dependencies))
if (dependencies.isEmpty()) {
InfoComposable(
title = AnnotatedString(text = stringResource(R.string.details_no_dependencies))
)
} else {
LazyRow(modifier = Modifier.fillMaxWidth()) {
items(items = dependencies, key = { item -> item.id }) { app ->
AppComposable(
app = app,
onClick = { onNavigateToAppDetails(app.packageName) }
)
}
}
}
}
/**
* Composable to show more information about the app that maybe advanced
*/
@Composable
private fun AppInfoMore(app: App) {
HeaderComposable(title = stringResource(R.string.details_more_info))
InfoComposable(
title = AnnotatedString(
text = stringResource(R.string.details_more_package_name)
),
description = AnnotatedString(text = app.packageName)
)
InfoComposable(
title = AnnotatedString(
text = stringResource(R.string.details_more_target_api)
),
description = AnnotatedString(text = "API ${app.targetSdk}")
)
InfoComposable(
title = AnnotatedString(
text = stringResource(R.string.details_more_content_rating)
),
description = AnnotatedString(text = app.contentRating.title)
)
app.appInfo.appInfoMap.forEach { (title, subtitle) ->
InfoComposable(
title = AnnotatedString(
text = title.replace("_", " ")
.lowercase(Locale.getDefault())
.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
),
description = AnnotatedString(text = subtitle)
)
}
}
@Preview
@Composable
@OptIn(ExperimentalCoilApi::class)

View File

@@ -18,6 +18,8 @@ import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -34,26 +36,38 @@ import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import androidx.window.core.layout.WindowWidthSizeClass
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.gplayapi.data.models.Review
import com.aurora.store.R
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.compose.composables.details.ReviewComposable
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.aurora.store.viewmodel.review.DetailsReviewViewModel
import kotlinx.coroutines.flow.flowOf
@Composable
fun DetailsReviewScreen(
onNavigateUp: () -> Unit,
viewModel: AppDetailsViewModel = hiltViewModel()
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(),
detailsReviewViewModel: DetailsReviewViewModel = hiltViewModel { factory: DetailsReviewViewModel.Factory ->
factory.create(appDetailsViewModel.app.value!!.packageName)
},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val app by viewModel.app.collectAsStateWithLifecycle()
val reviews = viewModel.reviews.collectAsLazyPagingItems()
val app by appDetailsViewModel.app.collectAsStateWithLifecycle()
val reviews = detailsReviewViewModel.reviews.collectAsLazyPagingItems()
val topAppBarTitle = when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> app!!.displayName
else -> stringResource(R.string.details_ratings)
}
ScreenContent(
topAppBarTitle = app!!.displayName,
topAppBarTitle = topAppBarTitle,
reviews = reviews,
onNavigateUp = onNavigateUp,
onFilter = { filter -> viewModel.fetchReviews(filter) }
onFilter = { filter -> detailsReviewViewModel.fetchReviews(filter) }
)
}
@@ -62,12 +76,17 @@ private fun ScreenContent(
topAppBarTitle: String? = null,
onNavigateUp: () -> Unit = {},
reviews: LazyPagingItems<Review>,
onFilter: (filter: Review.Filter) -> Unit = {}
onFilter: (filter: Review.Filter) -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
Scaffold(
topBar = {
TopAppBarComposable(title = topAppBarTitle, onNavigateUp = onNavigateUp)
TopAppBarComposable(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
}
) { paddingValues ->
Column(
@@ -90,7 +109,7 @@ private fun ScreenContent(
count = reviews.itemCount,
key = reviews.itemKey { it.commentId }
) { index ->
ReviewComposable(review = reviews[index]!!)
reviews[index]?.let { review -> ReviewComposable(review = review) }
}
}
}

View File

@@ -10,7 +10,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -18,6 +21,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.core.layout.WindowWidthSizeClass
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.R
import com.aurora.store.compose.composables.TopAppBarComposable
@@ -28,12 +33,18 @@ import com.aurora.store.viewmodel.details.AppDetailsViewModel
fun DetailsScreenshotScreen(
index: Int,
onNavigateUp: () -> Unit,
viewModel: AppDetailsViewModel = hiltViewModel()
viewModel: AppDetailsViewModel = hiltViewModel(),
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val app by viewModel.app.collectAsStateWithLifecycle()
val topAppBarTitle = when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> app!!.displayName
else -> stringResource(R.string.details_more_about_app)
}
ScreenContent(
topAppBarTitle = app!!.displayName,
topAppBarTitle = topAppBarTitle,
onNavigateUp = onNavigateUp,
screenshots = app!!.screenshots,
index = index
@@ -45,14 +56,23 @@ private fun ScreenContent(
topAppBarTitle: String? = null,
onNavigateUp: () -> Unit = {},
screenshots: List<Artwork> = emptyList(),
index: Int = 0
index: Int = 0,
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val displayMetrics = LocalContext.current.resources.displayMetrics
val pagerState = rememberPagerState(initialPage = index) { screenshots.size }
LaunchedEffect(key1 = index) {
if (pagerState.currentPage != index) pagerState.scrollToPage(index)
}
Scaffold(
topBar = {
TopAppBarComposable(title = topAppBarTitle, onNavigateUp = onNavigateUp)
TopAppBarComposable(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
}
) { paddingValues ->
HorizontalPager(

View File

@@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.dev
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.store.R
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.viewmodel.details.DevProfileViewModel
@Composable
fun DevProfileScreen(
developerId: String,
onNavigateUp: () -> Unit,
onNavigateToAppDetails: (packageName: String) -> Unit,
devProfileViewModel: DevProfileViewModel = hiltViewModel()
) {
ScreenContent(
topAppBarTitle = "",
onNavigateUp = onNavigateUp,
onNavigateToAppDetails = onNavigateToAppDetails
)
}
@Composable
private fun ScreenContent(
topAppBarTitle: String? = null,
onNavigateUp: () -> Unit = {},
onNavigateToAppDetails: (packageName: String) -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
Scaffold(
topBar = {
TopAppBarComposable(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) {
}
}
}
@Preview
@Composable
private fun DevProfileScreenPreview() {
ScreenContent()
}

View File

@@ -26,7 +26,6 @@ sealed class BusEvent : Event() {
lateinit var error: String
data class Blacklisted(val packageName: String) : BusEvent()
data class ManualDownload(val packageName: String, val versionCode: Long) : BusEvent()
}
sealed class AuthEvent : Event() {

View File

@@ -35,7 +35,6 @@ object Preferences {
const val PREFERENCE_THEME_STYLE = "PREFERENCE_THEME_STYLE"
const val PREFERENCE_FOR_YOU = "PREFERENCE_FOR_YOU"
const val PREFERENCE_DEFAULT_SELECTED_TAB = "PREFERENCE_DEFAULT_SELECTED_TAB"
const val PREFERENCE_SIMILAR = "PREFERENCE_SIMILAR"
const val PREFERENCE_INTRO = "PREFERENCE_INTRO"
const val PREFERENCE_FILTER_FDROID = "PREFERENCE_FILTER_FDROID"

View File

@@ -1,54 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.controller
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.view.epoxy.groups.CarouselShimmerGroup
import com.aurora.store.view.epoxy.groups.DeveloperModelGroup
open class DeveloperCarouselController(private val callbacks: Callbacks) :
GenericCarouselController(callbacks) {
override fun applyFilter(streamBundle: StreamCluster): Boolean {
return streamBundle.clusterTitle.isNotBlank() //Filter noisy cluster
&& streamBundle.clusterAppList.isNotEmpty() //Filter empty clusters
}
override fun buildModels(streamBundle: StreamBundle?) {
setFilterDuplicates(true)
if (streamBundle == null) {
for (i in 1..2) {
add(
CarouselShimmerGroup()
.id(i)
)
}
} else {
streamBundle
.streamClusters
.values
.filter { applyFilter(it) }
.forEach { streamCluster ->
add(DeveloperModelGroup(streamCluster, callbacks))
}
}
}
}

View File

@@ -1,128 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.groups
import android.util.Log
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyModelGroup
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.R
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.app.AppViewModel_
import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_
class DeveloperModelGroup(
streamCluster: StreamCluster,
callbacks: GenericCarouselController.Callbacks
) :
EpoxyModelGroup(
R.layout.model_developer_carousel_group, buildModels(
streamCluster,
callbacks
)
) {
companion object {
private const val TAG = "DeveloperModelGroup"
private fun buildModels(
streamCluster: StreamCluster,
callbacks: GenericCarouselController.Callbacks
): List<EpoxyModel<*>> {
val models = ArrayList<EpoxyModel<*>>()
val clusterViewModels = mutableListOf<EpoxyModel<*>>()
val screenshotsViewModels = mutableListOf<EpoxyModel<*>>()
val idPrefix = streamCluster.id
models.add(
HeaderViewModel_()
.id("${idPrefix}_header")
.title(streamCluster.clusterTitle)
.browseUrl(streamCluster.clusterBrowseUrl)
.click { _ ->
callbacks.onHeaderClicked(streamCluster)
}
)
if (streamCluster.clusterAppList.size == 1) {
val app = streamCluster.clusterAppList[0]
for (artwork in app.screenshots) {
screenshotsViewModels.add(
ScreenshotViewModel_()
.id(artwork.url)
.artwork(artwork)
)
}
clusterViewModels.add(
AppListViewModel_()
.id(app.id)
.app(app)
.click { _ ->
callbacks.onAppClick(app)
}
)
} else {
for (app in streamCluster.clusterAppList) {
clusterViewModels.add(
AppViewModel_()
.id(app.id)
.app(app)
.click { _ ->
callbacks.onAppClick(app)
}
.longClick { _ ->
callbacks.onAppLongClick(app)
false
}
.onBind { _, _, position ->
val itemCount = clusterViewModels.count()
if (itemCount >= 2) {
if (position == clusterViewModels.count() - 2) {
callbacks.onClusterScrolled(streamCluster)
Log.i(TAG, "Cluster ${streamCluster.clusterTitle} Scrolled")
}
}
}
)
}
}
if (screenshotsViewModels.isNotEmpty()) {
models.add(
CarouselHorizontalModel_()
.id("${idPrefix}_screenshots")
.models(screenshotsViewModels)
)
}
models.add(
CarouselHorizontalModel_()
.id("${idPrefix}_cluster")
.models(clusterViewModels)
)
return models
}
}
}

View File

@@ -1,58 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.OnViewRecycled
import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.databinding.ViewExodusBinding
import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class
)
class ExodusView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewExodusBinding>(context, attrs, defStyleAttr) {
@ModelProp
fun tracker(report: ExodusTracker) {
binding.txtTitle.text = report.name
binding.txtSubtitle.text = report.signature
binding.txtDescription.text = report.date
}
@CallbackProp
fun click(onClickListener: OnClickListener?) {
binding.root.setOnClickListener(onClickListener)
}
@OnViewRecycled
fun clear() {
}
}

View File

@@ -1,47 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.databinding.ViewFileBinding
import com.aurora.store.util.CommonUtil
import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class
)
class FileView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewFileBinding>(context, attrs, defStyleAttr) {
@ModelProp
fun file(file: PlayFile) {
binding.line1.text = file.name
binding.line2.text = CommonUtil.addSiPrefix(file.size)
}
}

View File

@@ -1,52 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.content.res.Resources
import android.util.AttributeSet
import coil3.load
import coil3.request.placeholder
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.R
import com.aurora.store.databinding.ViewScreenshotLargeBinding
import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_MATCH_HEIGHT,
baseModelClass = BaseModel::class
)
class LargeScreenshotView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewScreenshotLargeBinding>(context, attrs, defStyleAttr) {
@ModelProp
fun artwork(artwork: Artwork) {
val displayMetrics = Resources.getSystem().displayMetrics
binding.img.load("${artwork.url}=rw-w${displayMetrics.widthPixels}-v1-e15") {
placeholder(R.drawable.bg_placeholder)
}
}
}

View File

@@ -1,102 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
import coil3.load
import coil3.request.placeholder
import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.extensions.px
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.R
import com.aurora.store.databinding.ViewScreenshotMiniBinding
import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class
)
class MiniScreenshotView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewScreenshotMiniBinding>(context, attrs, defStyleAttr) {
private var position: Int = 0
interface ScreenshotCallback {
fun onClick(position: Int = 0)
}
@ModelProp
fun position(pos: Int) {
position = pos
}
@ModelProp
fun artwork(artwork: Artwork) {
normalizeSize(artwork)
binding.img.load("${artwork.url}=rw-w480-v1-e15") {
placeholder(R.drawable.bg_rounded)
transformations(RoundedCornersTransformation(8.px.toFloat()))
}
}
private fun normalizeSize(artwork: Artwork) {
if (artwork.height != 0 && artwork.width != 0) {
val artworkHeight = artwork.height
val artworkWidth = artwork.width
val normalizedHeight: Float
val normalizedWidth: Float
when {
artworkHeight == artworkWidth -> {
normalizedHeight = 120f
normalizedWidth = 120f
}
else -> {
val factor = artworkHeight / 120f
normalizedHeight = 120f
normalizedWidth = (artworkWidth / factor)
}
}
binding.img.layoutParams.height = normalizedHeight.px.toInt()
binding.img.layoutParams.width = normalizedWidth.px.toInt()
binding.img.requestLayout()
}
}
@CallbackProp
fun callback(screenshotCallback: ScreenshotCallback?) {
binding.img.setOnClickListener {
screenshotCallback?.onClick(position)
}
}
}

View File

@@ -1,71 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
import coil3.load
import coil3.request.placeholder
import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.extensions.toDate
import com.aurora.gplayapi.data.models.Review
import com.aurora.store.R
import com.aurora.store.databinding.ViewReviewBinding
import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class
)
class ReviewView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewReviewBinding>(context, attrs, defStyleAttr) {
@ModelProp
fun review(review: Review) {
binding.txtAuthor.text = review.userName
binding.txtTime.text = ("${review.timeStamp.toDate()} • v${review.appVersion}")
binding.txtComment.text = review.comment
binding.img.load(review.userPhotoUrl) {
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(32F))
}
binding.rating.rating = review.rating.toFloat()
}
@CallbackProp
fun click(onClickListener: OnClickListener?) {
binding.root.setOnClickListener(onClickListener)
}
@CallbackProp
fun longClick(onClickListener: OnLongClickListener?) {
binding.root.setOnLongClickListener(onClickListener)
}
}

View File

@@ -1,102 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
import coil3.load
import coil3.request.placeholder
import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.extensions.px
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.R
import com.aurora.store.databinding.ViewScreenshotBinding
import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class
)
class ScreenshotView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewScreenshotBinding>(context, attrs, defStyleAttr) {
private var position: Int = 0
interface ScreenshotCallback {
fun onClick(position: Int = 0)
}
@ModelProp
fun position(pos: Int) {
position = pos
}
@ModelProp
fun artwork(artwork: Artwork) {
normalizeSize(artwork)
binding.img.load("${artwork.url}=rw-w480-v1-e15") {
placeholder(R.drawable.bg_rounded)
transformations(RoundedCornersTransformation(8.px.toFloat()))
}
}
private fun normalizeSize(artwork: Artwork) {
if (artwork.height != 0 && artwork.width != 0) {
val artworkHeight = artwork.height
val artworkWidth = artwork.width
val normalizedHeight: Float
val normalizedWidth: Float
when {
artworkHeight == artworkWidth -> {
normalizedHeight = 192f
normalizedWidth = 192f
}
else -> {
val factor = artworkHeight / 192f
normalizedHeight = 192f
normalizedWidth = (artworkWidth / factor)
}
}
binding.img.layoutParams.height = normalizedHeight.px.toInt()
binding.img.layoutParams.width = normalizedWidth.px.toInt()
binding.img.requestLayout()
}
}
@CallbackProp
fun callback(screenshotCallback: ScreenshotCallback?) {
binding.img.setOnClickListener {
screenshotCallback?.onClick(position)
}
}
}

View File

@@ -121,15 +121,6 @@ abstract class BaseFragment<ViewBindingType : ViewBinding> : Fragment() {
)
}
fun openScreenshotFragment(app: App, position: Int) {
findNavController().navigate(
MobileNavigationDirections.actionGlobalScreenshotFragment(
position,
app.screenshots.toTypedArray()
)
)
}
fun openAppMenuSheet(app: MinimalApp) {
findNavController().navigate(MobileNavigationDirections.actionGlobalAppMenuSheet(app))
}

View File

@@ -24,15 +24,11 @@ import android.view.View
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.airbnb.epoxy.EpoxyModel
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.groups.CarouselHorizontalModel_
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.details.MiniScreenshotView
import com.aurora.store.view.epoxy.views.details.MiniScreenshotViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.viewmodel.browse.ExpandedStreamBrowseViewModel
import dagger.hilt.android.AndroidEntryPoint
@@ -92,30 +88,6 @@ class ExpandedStreamBrowseFragment : BaseFragment<FragmentGenericWithToolbarBind
}
} else {
streamCluster.clusterAppList.forEach {
val screenshotsViewModels = mutableListOf<EpoxyModel<*>>()
for ((position, artwork) in it.screenshots.withIndex()) {
screenshotsViewModels.add(
MiniScreenshotViewModel_()
.id(artwork.url)
.position(position)
.artwork(artwork)
.callback(object : MiniScreenshotView.ScreenshotCallback {
override fun onClick(position: Int) {
openScreenshotFragment(it, position)
}
})
)
}
if (screenshotsViewModels.isNotEmpty()) {
add(
CarouselHorizontalModel_()
.id("${it.id}_screenshots")
.models(screenshotsViewModels)
)
}
add(
AppListViewModel_()
.id(it.packageName.hashCode())

View File

@@ -63,7 +63,6 @@ import com.aurora.store.AppStreamStash
import com.aurora.store.AuroraApp
import com.aurora.store.MainActivity
import com.aurora.store.R
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.Event
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.installer.AppInstaller
@@ -77,14 +76,11 @@ import com.aurora.store.util.CommonUtil
import com.aurora.store.util.FlavouredUtil
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_SIMILAR
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED
import com.aurora.store.util.ShortcutManagerUtil
import com.aurora.store.view.custom.RatingView
import com.aurora.store.view.epoxy.controller.DetailsCarouselController
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.view.epoxy.views.details.ScreenshotView
import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.aurora.store.viewmodel.details.DetailsClusterViewModel
@@ -114,8 +110,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
private val isExtendedUpdateEnabled: Boolean
get() = Preferences.getBoolean(requireContext(), PREFERENCE_UPDATES_EXTENDED)
private val showSimilarApps: Boolean
get() = Preferences.getBoolean(requireContext(), PREFERENCE_SIMILAR)
private fun onEvent(event: Event) {
when (event) {
@@ -131,20 +125,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
}
}
is BusEvent.ManualDownload -> {
if (app.packageName == event.packageName) {
val requestedApp = app.copy(
versionCode = event.versionCode,
dependencies = app.dependencies.copy(
dependentLibraries = app.dependencies.dependentLibraries.map { lib ->
lib.copy(versionCode = event.versionCode)
}
)
)
purchase(requestedApp)
}
}
is InstallerEvent.Failed -> {
if (app.packageName == event.packageName) {
findNavController().navigate(
@@ -241,16 +221,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
text =
"${report.trackers.size} ${getString(R.string.exodus_substring)} ${report.version}"
}
binding.layoutDetailsPrivacy.headerPrivacy.addClickListener {
findNavController().navigate(
AppDetailsFragmentDirections
.actionAppDetailsFragmentToDetailsExodusFragment(
app.displayName,
report
)
)
}
} else {
binding.layoutDetailsPrivacy.txtStatus.apply {
setTextColor(ContextCompat.getColor(requireContext(), R.color.colorGreen))
@@ -618,16 +588,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
purchase(app)
}
}
binding.layoutDetailsApp.btnSecondaryAction.apply {
text = getString(R.string.title_manual_download)
setOnClickListener {
findNavController().navigate(
AppDetailsFragmentDirections
.actionAppDetailsFragmentToManualDownloadSheet(app)
)
}
}
}
// Lay out the toolbar again
@@ -651,7 +611,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
}
private fun updateExtraDetails(app: App) {
binding.viewFlipper.displayedChild = 1
updateAppDescription(app)
updateAppRatingAndReviews(app)
@@ -671,10 +630,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
updateBetaSubscription(app)
}
if (showSimilarApps) {
updateAppStream(app)
}
checkAndSetupInstall()
}
@@ -706,45 +661,11 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
)
}
}
headerDescription.addClickListener {
findNavController().navigate(
AppDetailsFragmentDirections.actionAppDetailsFragmentToDetailsMoreFragment(app)
)
}
epoxyRecycler.withModels {
setFilterDuplicates(true)
var position = 0
app.screenshots
//.sortedWith { o1, o2 -> o2.height.compareTo(o1.height) }
.forEach { artwork ->
add(
ScreenshotViewModel_()
.id(artwork.url)
.artwork(artwork)
.position(position++)
.callback(object : ScreenshotView.ScreenshotCallback {
override fun onClick(position: Int) {
openScreenshotFragment(app, position)
}
})
)
}
}
}
}
private fun updateAppRatingAndReviews(app: App) {
binding.layoutDetailsReview.apply {
headerRatingReviews.addClickListener {
findNavController().navigate(
AppDetailsFragmentDirections.actionAppDetailsFragmentToDetailsReviewFragment(
app.displayName,
app.packageName
)
)
}
var totalStars = 0L
totalStars += app.rating.oneStar
@@ -833,15 +754,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
imgBeta.load(betaProgram.artwork.url) {
}
btnBetaAction.setOnClickListener {
btnBetaAction.text = getString(R.string.action_pending)
btnBetaAction.isEnabled = false
viewModel.fetchTestingProgramStatus(
app.packageName,
!betaProgram.isSubscribed
)
}
} else {
root.hide()
}
@@ -956,7 +868,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
private fun updateCompatibilityInfo() {
if (app.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS)) {
viewModel.fetchPlexusReport(app.packageName)
binding.layoutDetailsCompatibility.txtGmsDependency.apply {
title = getString(R.string.details_compatibility_gms_required_title)

View File

@@ -1,98 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.details
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.aurora.Constants
import com.aurora.extensions.browse
import com.aurora.store.R
import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.data.model.Report
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.details.ExodusViewModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.details.DetailsExodusViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class DetailsExodusFragment : BaseFragment<FragmentGenericWithToolbarBinding>() {
private val viewModel: DetailsExodusViewModel by viewModels()
private val args: DetailsExodusFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Toolbar
binding.toolbar.apply {
title = args.displayName
setNavigationOnClickListener { findNavController().navigateUp() }
}
updateController(getExodusTrackersFromReport(args.report))
}
private fun updateController(reviews: List<ExodusTracker>) {
binding.recycler.withModels {
add(
HeaderViewModel_()
.id("header")
.title(getString(R.string.exodus_view_report))
.browseUrl("browse")
.click { _ -> context?.browse(Constants.EXODUS_REPORT_URL + args.report.id) }
)
reviews.forEach {
add(
ExodusViewModel_()
.id(it.id)
.tracker(it)
.click { _ ->
context?.browse(it.url)
}
)
}
}
}
private fun getExodusTrackersFromReport(report: Report): List<ExodusTracker> {
val trackerObjects = report.trackers.map {
viewModel.exodusTrackers.getJSONObject(it.toString())
}.toList()
return trackerObjects.map {
ExodusTracker(
id = it.getInt("id"),
name = it.getString("name"),
url = it.getString("website"),
signature = it.getString("code_signature"),
date = it.getString("creation_date"),
description = it.getString("description"),
networkSignature = it.getString("network_signature"),
documentation = listOf(it.getString("documentation")),
categories = listOf(it.getString("categories"))
)
}.toList()
}
}

View File

@@ -1,172 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.details
import android.os.Bundle
import android.view.View
import androidx.core.text.HtmlCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.databinding.FragmentDetailsMoreBinding
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.app.NoAppAltViewModel_
import com.aurora.store.view.epoxy.views.details.AppDependentViewModel_
import com.aurora.store.view.epoxy.views.details.FileViewModel_
import com.aurora.store.view.epoxy.views.details.InfoViewModel_
import com.aurora.store.view.epoxy.views.details.MoreBadgeViewModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.details.DetailsMoreViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.util.UUID
@AndroidEntryPoint
class DetailsMoreFragment : BaseFragment<FragmentDetailsMoreBinding>() {
private val viewModel: DetailsMoreViewModel by viewModels()
private val args: DetailsMoreFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Adjust layout for edgeToEdge display
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerMore) { layout, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
layout.setPadding(0, 0, 0, insets.bottom)
WindowInsetsCompat.CONSUMED
}
// Toolbar
binding.toolbar.setOnClickListener { findNavController().navigateUp() }
inflateDescription(args.app)
inflateFiles(args.app)
viewModel.fetchDependentApps(args.app)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.dependentApps.collect { list ->
if (list.isNotEmpty()) {
binding.recyclerDependency.withModels {
list.filter { it.displayName.isNotEmpty() }.forEach {
add(
AppDependentViewModel_()
.id(it.id)
.app(it)
.click { _ -> openDetailsFragment(it.packageName, it) }
)
}
}
} else {
binding.recyclerDependency.withModels {
add(
NoAppAltViewModel_()
.id("no_app")
.message(getString(R.string.details_no_dependencies))
)
}
}
}
}
}
private fun inflateDescription(app: App) {
binding.toolbar.title = app.displayName
binding.txtDescription.text = HtmlCompat.fromHtml(
app.description,
HtmlCompat.FROM_HTML_MODE_COMPACT
)
}
private fun inflateFiles(app: App) {
binding.recyclerMore.withModels {
//Add dependent files
if (app.fileList.isNotEmpty()) {
add(
HeaderViewModel_()
.id("badge_header")
.title("Files")
)
app.fileList.forEach {
add(
FileViewModel_()
.id(it.id)
.file(it)
)
}
}
//Add display & extra badges
if (app.infoBadges.isNotEmpty()) {
add(
HeaderViewModel_()
.id("badge_header")
.title("More")
)
app.infoBadges.forEach {
add(
MoreBadgeViewModel_()
.id(it.id)
.badge(it)
)
}
if (app.displayBadges.isNotEmpty()) {
app.displayBadges
.filter { it.textMajor.isNotEmpty() }
.forEach {
add(
MoreBadgeViewModel_()
.id(it.id)
.badge(it)
)
}
}
}
if (app.appInfo.appInfoMap.isNotEmpty()) {
add(
HeaderViewModel_()
.id("info_header")
.title("Info")
)
app.appInfo.appInfoMap.forEach {
add(
InfoViewModel_()
.id(it.key)
.badge(it)
)
}
add(
InfoViewModel_()
.id(UUID.randomUUID().toString())
.badge(mapOf("TARGET" to "${app.targetSdk}").entries.first())
)
}
}
}
}

View File

@@ -1,126 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.details
import android.os.Bundle
import android.view.View
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.ReviewCluster
import com.aurora.store.R
import com.aurora.store.databinding.FragmentDetailsReviewBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
import com.aurora.store.view.epoxy.views.details.ReviewViewModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.review.ReviewViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class DetailsReviewFragment : BaseFragment<FragmentDetailsReviewBinding>() {
private val args: DetailsReviewFragmentArgs by navArgs()
private val viewModel: ReviewViewModel by viewModels()
private lateinit var endlessRecyclerOnScrollListener: EndlessRecyclerOnScrollListener
private lateinit var filter: Review.Filter
private lateinit var reviewCluster: ReviewCluster
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Adjust layout for edgeToEdge display
ViewCompat.setOnApplyWindowInsetsListener(binding.recycler) { layout, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
layout.setPadding(0, 0, 0, insets.bottom)
WindowInsetsCompat.CONSUMED
}
// Toolbar
binding.toolbar.apply {
title = args.displayName
setNavigationOnClickListener { findNavController().navigateUp() }
}
viewModel.liveData.observe(viewLifecycleOwner) {
if (!::reviewCluster.isInitialized) {
endlessRecyclerOnScrollListener = object : EndlessRecyclerOnScrollListener() {
override fun onLoadMore(currentPage: Int) {
if (::reviewCluster.isInitialized) {
viewModel.next(reviewCluster.nextPageUrl)
}
}
}
binding.recycler.addOnScrollListener(endlessRecyclerOnScrollListener)
}
it?.let {
reviewCluster = it
updateController(reviewCluster)
}
}
// Fetch Reviews
filter = Review.Filter.ALL
viewModel.fetchReview(args.packageName, filter)
// Chips
binding.chipGroup.setOnCheckedStateChangeListener { _, checkedIds ->
when (checkedIds[0]) {
R.id.filter_review_all -> filter = Review.Filter.ALL
R.id.filter_newest_first -> filter = Review.Filter.NEWEST
R.id.filter_review_critical -> filter = Review.Filter.CRITICAL
R.id.filter_review_positive -> filter = Review.Filter.POSITIVE
R.id.filter_review_five -> filter = Review.Filter.FIVE
R.id.filter_review_four -> filter = Review.Filter.FOUR
R.id.filter_review_three -> filter = Review.Filter.THREE
R.id.filter_review_two -> filter = Review.Filter.TWO
R.id.filter_review_one -> filter = Review.Filter.ONE
}
endlessRecyclerOnScrollListener.resetPageCount()
viewModel.fetchReview(args.packageName, filter)
}
}
private fun updateController(reviewCluster: ReviewCluster) {
binding.recycler.withModels {
setFilterDuplicates(true)
reviewCluster.reviewList.forEach {
add(
ReviewViewModel_()
.id(it.commentId)
.review(it)
)
}
if (reviewCluster.hasNext()) {
add(
AppProgressViewModel_()
.id("progress")
)
}
}
}
}

View File

@@ -31,7 +31,6 @@ import com.aurora.gplayapi.data.models.details.DevStream
import com.aurora.store.R
import com.aurora.store.data.model.ViewState
import com.aurora.store.databinding.FragmentDevProfileBinding
import com.aurora.store.view.epoxy.controller.DeveloperCarouselController
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.details.DevProfileViewModel
@@ -47,7 +46,6 @@ class DevProfileFragment : BaseFragment<FragmentDevProfileBinding>(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val developerCarouselController = DeveloperCarouselController(this)
// Toolbar
binding.toolbar.apply {
@@ -56,7 +54,6 @@ class DevProfileFragment : BaseFragment<FragmentDevProfileBinding>(),
}
// RecyclerView
binding.recycler.setController(developerCarouselController)
viewModel.liveData.observe(viewLifecycleOwner) {
when (it) {
@@ -82,7 +79,6 @@ class DevProfileFragment : BaseFragment<FragmentDevProfileBinding>(),
binding.txtDevDescription.text = description
binding.imgIcon.load(imgUrl)
binding.viewFlipper.displayedChild = 0
developerCarouselController.setData(streamBundle)
}
}
}

View File

@@ -1,73 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.details
import android.os.Bundle
import android.view.View
import androidx.core.content.ContextCompat
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.R
import com.aurora.store.databinding.FragmentScreenshotBinding
import com.aurora.store.view.epoxy.views.details.LargeScreenshotViewModel_
import com.aurora.store.view.ui.commons.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ScreenshotFragment : BaseFragment<FragmentScreenshotBinding>() {
private val args: ScreenshotFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Toolbar
binding.toolbar.apply {
elevation = 0f
navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back)
setNavigationOnClickListener { findNavController().navigateUp() }
}
// Recycler View
binding.recyclerView.apply {
layoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
PagerSnapHelper().attachToRecyclerView(this)
}
updateController(args.arrayOfArtwork, args.position)
}
private fun updateController(artworks: Array<Artwork>, position: Int) {
binding.recyclerView.withModels {
artworks.forEach {
add(
LargeScreenshotViewModel_()
.id(it.url)
.artwork(it)
)
}
binding.recyclerView.scrollToPosition(position)
}
}
}

View File

@@ -1,106 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2019, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
package com.aurora.store.view.ui.sheets
import android.app.Dialog
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
import coil3.load
import coil3.request.placeholder
import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import com.aurora.extensions.toast
import com.aurora.store.R
import com.aurora.store.databinding.SheetManualDownloadBinding
import com.aurora.store.viewmodel.sheets.ManualDownloadViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@AndroidEntryPoint
class ManualDownloadSheet : BaseDialogSheet<SheetManualDownloadBinding>() {
private val viewModel: ManualDownloadViewModel by viewModels()
private val args: ManualDownloadSheetArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isCancelable = false
inflateData()
attachActions()
viewLifecycleOwner.lifecycleScope.launch {
viewModel.purchaseStatus.collectLatest {
if (it) {
toast(R.string.toast_manual_available)
dismissAllowingStateLoss()
} else {
toast(R.string.toast_manual_unavailable)
}
}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
this.lifecycleScope.launch {
viewModel.purchaseStatus.collectLatest {
if (it) {
toast(R.string.toast_manual_available)
dismissAllowingStateLoss()
} else {
toast(R.string.toast_manual_unavailable)
}
}
}
return super.onCreateDialog(savedInstanceState)
}
private fun inflateData() {
binding.imgIcon.load(args.app.iconArtwork.url) {
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(32F))
}
binding.txtLine1.text = args.app.displayName
binding.txtLine2.text = args.app.packageName
binding.txtLine3.text = ("${args.app.versionName} (${args.app.versionCode})")
binding.versionCodeLayout.hint = "${args.app.versionCode}"
binding.versionCodeLayout.editText?.setText("${args.app.versionCode}")
}
private fun attachActions() {
binding.btnPrimary.setOnClickListener {
val customVersionString = (binding.versionCodeInp.text).toString()
if (customVersionString.isEmpty())
binding.versionCodeInp.error = "Enter version code"
else {
viewModel.purchase(args.app, customVersionString.toLong())
}
}
binding.btnSecondary.setOnClickListener {
dismissAllowingStateLoss()
}
}
}

View File

@@ -7,11 +7,10 @@
package com.aurora.store.viewmodel.details
import android.content.Context
import android.content.pm.PackageInstaller
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.aurora.Constants
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Review
@@ -24,11 +23,9 @@ import com.aurora.gplayapi.network.IHttpClient
import com.aurora.store.BuildConfig
import com.aurora.store.data.helper.DownloadHelper
import com.aurora.store.data.model.ExodusReport
import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.data.model.PlexusReport
import com.aurora.store.data.model.Report
import com.aurora.store.data.model.Scores
import com.aurora.store.data.paging.GenericPagingSource.Companion.createPager
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.room.favourite.Favourite
import com.aurora.store.data.room.favourite.FavouriteDao
@@ -36,20 +33,18 @@ import com.aurora.store.util.CertUtil
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.PackageUtil.PACKAGE_NAME_GMS
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_SIMILAR
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
@@ -68,8 +63,7 @@ class AppDetailsViewModel @Inject constructor(
private val downloadHelper: DownloadHelper,
private val favouriteDao: FavouriteDao,
private val httpClient: IHttpClient,
private val json: Json,
private val exodusTrackers: JSONObject
private val json: Json
) : ViewModel() {
private val TAG = AppDetailsViewModel::class.java.simpleName
@@ -77,9 +71,8 @@ class AppDetailsViewModel @Inject constructor(
private val _app = MutableStateFlow<App?>(App(""))
val app = _app.asStateFlow()
private var reviewsNextPageUrl: String? = null
private val _reviews = MutableStateFlow<PagingData<Review>>(PagingData.empty())
val reviews = _reviews.asStateFlow()
private val _suggestions = MutableStateFlow<List<App>>(emptyList())
val suggestions = _suggestions.asStateFlow()
private val _userReview = MutableStateFlow<Review?>(null)
val userReview = _userReview.asStateFlow()
@@ -107,6 +100,27 @@ class AppDetailsViewModel @Inject constructor(
list.find { d -> d.packageName == a?.packageName }
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
// TODO: Save sessionId to downloads to monitor packageInstall progress
val installProgress = callbackFlow<Float?> {
val packageInstaller = context.packageManager.packageInstaller
val callback = object : PackageInstaller.SessionCallback() {
override fun onActiveChanged(p0: Int, p1: Boolean) {}
override fun onBadgingChanged(p0: Int) {}
override fun onCreated(p0: Int) {
// trySend(0F)
}
override fun onFinished(p0: Int, p1: Boolean) {
// trySend(null)
}
override fun onProgressChanged(p0: Int, p1: Float) {
// trySend(p1)
}
}
packageInstaller.registerSessionCallback(callback)
awaitClose { packageInstaller.unregisterSessionCallback(callback) }
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
private val isExtendedUpdateEnabled: Boolean
get() = Preferences.getBoolean(context, PREFERENCE_UPDATES_EXTENDED)
@@ -114,7 +128,7 @@ class AppDetailsViewModel @Inject constructor(
get() = PackageUtil.isUpdatable(
context,
app.value!!.packageName,
app.value!!.versionCode.toLong()
app.value!!.versionCode
)
private val hasValidCerts: Boolean
@@ -128,15 +142,9 @@ class AppDetailsViewModel @Inject constructor(
val hasValidUpdate: Boolean
get() = (isUpdatable && hasValidCerts) || (isUpdatable && isExtendedUpdateEnabled)
val showSimilarApps: Boolean
get() = Preferences.getBoolean(context, PREFERENCE_SIMILAR)
fun fetchAppDetails(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
_favourite.value = favouriteDao.isFavourite(packageName)
_dataSafetyReport.value = webDataSafetyHelper.fetch(packageName)
_app.value = appDetailsHelper.getAppByPackageName(packageName).copy(
isInstalled = PackageUtil.isInstalled(context, packageName)
)
@@ -148,7 +156,9 @@ class AppDetailsViewModel @Inject constructor(
// Only proceed if there was no error while fetching the app details
if (throwable != null) return@invokeOnCompletion
fetchReviews()
fetchFavourite(packageName)
fetchDataSafetyReport(packageName)
fetchSuggestions()
fetchExodusPrivacyReport(packageName)
if (app.value!!.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS)) {
fetchPlexusReport(packageName)
@@ -156,48 +166,11 @@ class AppDetailsViewModel @Inject constructor(
}
}
fun fetchReviews(filter: Review.Filter = Review.Filter.ALL) {
reviewsNextPageUrl = null
createPager {
val reviewsCluster = reviewsNextPageUrl?.let { nextPageUrl ->
reviewsHelper.next(nextPageUrl)
} ?: reviewsHelper.getReviews(app.value!!.packageName, filter)
reviewsNextPageUrl = reviewsCluster.nextPageUrl
reviewsCluster.reviewList
}.flow.distinctUntilChanged()
.cachedIn(viewModelScope)
.onEach { _reviews.value = it }
.launchIn(viewModelScope)
}
private fun fetchExodusPrivacyReport(packageName: String) {
fun updateTestingProgramStatus(packageName: String, subscribe: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
try {
_exodusReport.value = getLatestExodusReport(packageName)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch privacy report", exception)
_exodusReport.value = null
}
}
}
fun fetchPlexusReport(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
_plexusScores.value = getPlexusReport(packageName)?.report?.scores
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch compatibility report", exception)
_plexusScores.value = null
}
}
}
fun fetchTestingProgramStatus(packageName: String, subscribe: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
try {
_testingProgramStatus.value = appDetailsHelper.testingProgram(packageName, subscribe)
_testingProgramStatus.value =
appDetailsHelper.testingProgram(packageName, subscribe)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch testing program status", exception)
}
@@ -272,24 +245,50 @@ class AppDetailsViewModel @Inject constructor(
}
}
fun getExodusTrackersFromReport(): List<ExodusTracker> {
val trackerObjects = exodusReport.value!!.trackers.map {
exodusTrackers.getJSONObject(it.toString())
}.toList()
private fun fetchFavourite(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
_favourite.value = favouriteDao.isFavourite(packageName)
}
}
return trackerObjects.map {
ExodusTracker(
id = it.getInt("id"),
name = it.getString("name"),
url = it.getString("website"),
signature = it.getString("code_signature"),
date = it.getString("creation_date"),
description = it.getString("description"),
networkSignature = it.getString("network_signature"),
documentation = listOf(it.getString("documentation")),
categories = listOf(it.getString("categories"))
)
}.toList()
private fun fetchDataSafetyReport(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
_dataSafetyReport.value = webDataSafetyHelper.fetch(packageName)
}
}
private fun fetchSuggestions() {
// Bail out if we go no suggestions to offer
if (app.value!!.detailsStreamUrl.isNullOrBlank()) return
viewModelScope.launch(Dispatchers.IO) {
val streamBundle = appDetailsHelper.getDetailsStream(app.value!!.detailsStreamUrl!!)
_suggestions.value = streamBundle.streamClusters.values
.flatMap { it.clusterAppList }
.distinctBy { it.packageName }
}
}
private fun fetchExodusPrivacyReport(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
_exodusReport.value = getLatestExodusReport(packageName)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch privacy report", exception)
_exodusReport.value = null
}
}
}
private fun fetchPlexusReport(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
_plexusScores.value = getPlexusReport(packageName)?.report?.scores
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch compatibility report", exception)
_plexusScores.value = null
}
}
}
private fun getLatestExodusReport(packageName: String): Report? {
@@ -305,12 +304,6 @@ class AppDetailsViewModel @Inject constructor(
.firstOrNull()
}
private fun getPlexusReport(packageName: String): PlexusReport? {
val url = "${Constants.PLEXUS_API_URL}/${packageName}/?scores=true"
val playResponse = httpClient.get(url, emptyMap())
return json.decodeFromString<PlexusReport>(String(playResponse.responseBytes))
}
private fun parseExodusResponse(response: String, packageName: String): List<Report> {
try {
val jsonObject = JSONObject(response)
@@ -322,4 +315,10 @@ class AppDetailsViewModel @Inject constructor(
return emptyList()
}
}
private fun getPlexusReport(packageName: String): PlexusReport? {
val url = "${Constants.PLEXUS_API_URL}/${packageName}/?scores=true"
val playResponse = httpClient.get(url, emptyMap())
return json.decodeFromString<PlexusReport>(String(playResponse.responseBytes))
}
}

View File

@@ -6,9 +6,56 @@
package com.aurora.store.viewmodel.details
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.data.model.Report
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.json.JSONObject
import javax.inject.Inject
@HiltViewModel
class DetailsExodusViewModel @Inject constructor(val exodusTrackers: JSONObject) : ViewModel()
@HiltViewModel(assistedFactory = DetailsExodusViewModel.Factory::class)
class DetailsExodusViewModel @AssistedInject constructor(
@Assisted private val exodusReport: Report,
private val exodusTrackers: JSONObject
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(exodusReport: Report): DetailsExodusViewModel
}
private val _trackers = MutableStateFlow<List<ExodusTracker>>(emptyList())
val trackers = _trackers.asStateFlow()
init {
getExodusTrackersFromReport()
}
fun getExodusTrackersFromReport() {
viewModelScope.launch(Dispatchers.IO) {
val trackerObjects = exodusReport.trackers.map {
exodusTrackers.getJSONObject(it.toString())
}.toList()
_trackers.value = trackerObjects.map {
ExodusTracker(
id = it.getInt("id"),
name = it.getString("name"),
url = it.getString("website"),
signature = it.getString("code_signature"),
date = it.getString("creation_date"),
description = it.getString("description"),
networkSignature = it.getString("network_signature"),
documentation = listOf(it.getString("documentation")),
categories = listOf(it.getString("categories"))
)
}.toList()
}
}
}

View File

@@ -1,3 +1,9 @@
/*
* SPDX-FileCopyrightText: 2023-2025 The Calyx Institute
* SPDX-FileCopyrightText: 2024 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.viewmodel.details
import android.util.Log
@@ -5,36 +11,42 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.AppDetailsHelper
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class DetailsMoreViewModel @Inject constructor(
@HiltViewModel(assistedFactory = DetailsMoreViewModel.Factory::class)
class DetailsMoreViewModel @AssistedInject constructor(
@Assisted private val dependencies: List<String>,
private val appDetailsHelper: AppDetailsHelper
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(dependencies: List<String>): DetailsMoreViewModel
}
private val TAG = DetailsMoreViewModel::class.java.simpleName
private val dependantAppsStash = mutableMapOf<String, List<App>>()
private val _dependentApps = MutableSharedFlow<List<App>>()
val dependentApps = _dependentApps.asSharedFlow()
private val _dependentApps = MutableStateFlow<List<App>?>(emptyList())
val dependentApps = _dependentApps.asStateFlow()
fun fetchDependentApps(app: App) {
init {
fetchDependencies()
}
private fun fetchDependencies() {
viewModelScope.launch(Dispatchers.IO) {
try {
val dependantApps = dependantAppsStash.getOrPut(app.packageName) {
appDetailsHelper.getAppByPackageName(app.dependencies.dependentPackages)
}
_dependentApps.emit(dependantApps)
_dependentApps.value = appDetailsHelper.getAppByPackageName(dependencies)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch dependencies", exception)
dependantAppsStash[app.packageName] = emptyList()
_dependentApps.emit(emptyList())
_dependentApps.value = null
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2021-2023 AuroraOSS
* SPDX-FileCopyrightText: 2024-2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.viewmodel.review
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.store.data.paging.GenericPagingSource.Companion.createPager
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@HiltViewModel(assistedFactory = DetailsReviewViewModel.Factory::class)
class DetailsReviewViewModel @AssistedInject constructor(
@Assisted private val packageName: String,
private val reviewsHelper: ReviewsHelper
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(packageName: String): DetailsReviewViewModel
}
private val TAG = DetailsReviewViewModel::class.java.simpleName
private var reviewsNextPageUrl: String? = null
private val _reviews = MutableStateFlow<PagingData<Review>>(PagingData.empty())
val reviews = _reviews.asStateFlow()
init {
fetchReviews()
}
fun fetchReviews(filter: Review.Filter = Review.Filter.ALL) {
reviewsNextPageUrl = null
createPager { page ->
try {
when (page) {
1 -> reviewsHelper.getReviews(packageName, filter).also {
reviewsNextPageUrl = it.nextPageUrl
}.reviewList
else -> {
if (!reviewsNextPageUrl.isNullOrBlank()) {
reviewsHelper.next(reviewsNextPageUrl!!).also {
reviewsNextPageUrl = it.nextPageUrl
}.reviewList
} else {
emptyList()
}
}
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch reviews for $page: $reviewsNextPageUrl", exception)
emptyList()
}
}.flow.distinctUntilChanged()
.cachedIn(viewModelScope)
.onEach { _reviews.value = it }
.launchIn(viewModelScope)
}
}

View File

@@ -1,72 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.viewmodel.review
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.ReviewCluster
import com.aurora.gplayapi.helpers.ReviewsHelper
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ReviewViewModel @Inject constructor(
private val reviewsHelper: ReviewsHelper
) : ViewModel() {
val TAG = javaClass.simpleName
val liveData: MutableLiveData<ReviewCluster> = MutableLiveData()
private lateinit var reviewsCluster: ReviewCluster
fun fetchReview(packageName: String, filter: Review.Filter) {
viewModelScope.launch(Dispatchers.IO) {
try {
reviewsCluster = reviewsHelper.getReviews(packageName, filter)
liveData.postValue(reviewsCluster)
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch reviews", e)
}
}
}
fun next(nextReviewPageUrl: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
val currentCluster = reviewsCluster
val nextReviewCluster = reviewsHelper.next(nextReviewPageUrl)
reviewsCluster = currentCluster.copy(
nextPageUrl = nextReviewCluster.nextPageUrl,
reviewList = currentCluster.reviewList + nextReviewCluster.reviewList
)
liveData.postValue(reviewsCluster)
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch next reviews $nextReviewPageUrl", e)
}
}
}
}

View File

@@ -1,43 +0,0 @@
package com.aurora.store.viewmodel.sheets
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.store.AuroraApp
import com.aurora.store.data.event.BusEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ManualDownloadViewModel @Inject constructor(
private val purchaseHelper: PurchaseHelper
) : ViewModel() {
private val TAG = ManualDownloadViewModel::class.java.simpleName
private val _purchaseStatus = MutableSharedFlow<Boolean>()
val purchaseStatus = _purchaseStatus.asSharedFlow()
fun purchase(app: App, customVersion: Long) {
viewModelScope.launch(Dispatchers.IO) {
try {
val files = purchaseHelper.purchase(app.packageName, customVersion, app.offerType)
if (files.isNotEmpty()) {
AuroraApp.events.send(
BusEvent.ManualDownload(app.packageName, customVersion)
)
}
_purchaseStatus.emit(files.isNotEmpty())
} catch (exception: Exception) {
_purchaseStatus.emit(false)
Log.e(TAG, "Failed to find version: $customVersion", exception)
}
}
}
}