compose: details: Split AppDetails screen logic into individual files
Make it more readable and independent to adapt for different devices Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.gplayapi.data.models.Artwork
|
import com.aurora.gplayapi.data.models.Artwork
|
||||||
import com.aurora.gplayapi.data.models.Rating
|
import com.aurora.gplayapi.data.models.Rating
|
||||||
|
import com.aurora.gplayapi.data.models.details.TestingProgram
|
||||||
import com.aurora.store.BuildConfig
|
import com.aurora.store.BuildConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,12 +63,18 @@ class AppPreviewProvider : PreviewParameterProvider<App> {
|
|||||||
developerWebsite = "https://auroraoss.com/",
|
developerWebsite = "https://auroraoss.com/",
|
||||||
developerAddress = "330 N Midland Ave, Mumbai, India",
|
developerAddress = "330 N Midland Ave, Mumbai, India",
|
||||||
screenshots = MutableList(5) { Artwork(url = "$it") },
|
screenshots = MutableList(5) { Artwork(url = "$it") },
|
||||||
|
testingProgram = TestingProgram(
|
||||||
|
isAvailable = true,
|
||||||
|
isSubscribed = false
|
||||||
|
),
|
||||||
rating = Rating(
|
rating = Rating(
|
||||||
fiveStar = 201458104,
|
fiveStar = 201458104,
|
||||||
fourStar = 313829104,
|
fourStar = 313829104,
|
||||||
threeStar = 204581672,
|
threeStar = 204581672,
|
||||||
twoStar = 183746829,
|
twoStar = 183746829,
|
||||||
oneStar = 96384291
|
oneStar = 96384291,
|
||||||
|
average = 4.4F,
|
||||||
|
abbreviatedLabel = "10 M"
|
||||||
),
|
),
|
||||||
permissions = mutableListOf(
|
permissions = mutableListOf(
|
||||||
Manifest.permission.INTERNET,
|
Manifest.permission.INTERNET,
|
||||||
|
|||||||
@@ -6,35 +6,14 @@
|
|||||||
package com.aurora.store.compose.ui.details
|
package com.aurora.store.compose.ui.details
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.text.format.Formatter
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
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
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
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.Scaffold
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
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.AnimatedPane
|
||||||
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole
|
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole
|
||||||
import androidx.compose.material3.adaptive.navigation.NavigableSupportingPaneScaffold
|
import androidx.compose.material3.adaptive.navigation.NavigableSupportingPaneScaffold
|
||||||
@@ -47,61 +26,48 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
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.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||||
import androidx.compose.ui.util.fastForEach
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.window.core.layout.WindowWidthSizeClass
|
|
||||||
import coil3.annotation.ExperimentalCoilApi
|
import coil3.annotation.ExperimentalCoilApi
|
||||||
import coil3.compose.LocalAsyncImagePreviewHandler
|
import coil3.compose.LocalAsyncImagePreviewHandler
|
||||||
import com.aurora.extensions.bodyVerySmall
|
|
||||||
import com.aurora.extensions.browse
|
|
||||||
import com.aurora.extensions.copyToClipBoard
|
|
||||||
import com.aurora.extensions.mailTo
|
|
||||||
import com.aurora.extensions.toast
|
import com.aurora.extensions.toast
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.gplayapi.data.models.Artwork
|
|
||||||
import com.aurora.gplayapi.data.models.Rating
|
|
||||||
import com.aurora.gplayapi.data.models.datasafety.EntryType
|
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composables.HeaderComposable
|
import com.aurora.store.compose.composables.HeaderComposable
|
||||||
import com.aurora.store.compose.composables.InfoComposable
|
|
||||||
import com.aurora.store.compose.composables.TopAppBarComposable
|
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.AppProgressComposable
|
||||||
import com.aurora.store.compose.composables.app.AppTagComposable
|
|
||||||
import com.aurora.store.compose.composables.app.NoAppComposable
|
import com.aurora.store.compose.composables.app.NoAppComposable
|
||||||
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.AppPreviewProvider
|
||||||
import com.aurora.store.compose.composables.preview.coilPreviewProvider
|
import com.aurora.store.compose.composables.preview.coilPreviewProvider
|
||||||
import com.aurora.store.compose.navigation.Screen
|
import com.aurora.store.compose.navigation.Screen
|
||||||
|
import com.aurora.store.compose.ui.details.components.AppActions
|
||||||
|
import com.aurora.store.compose.ui.details.components.AppChangelog
|
||||||
|
import com.aurora.store.compose.ui.details.components.AppCompatibility
|
||||||
|
import com.aurora.store.compose.ui.details.components.AppDataSafety
|
||||||
|
import com.aurora.store.compose.ui.details.components.AppDetails
|
||||||
|
import com.aurora.store.compose.ui.details.components.AppDeveloperDetails
|
||||||
|
import com.aurora.store.compose.ui.details.components.AppPrivacy
|
||||||
|
import com.aurora.store.compose.ui.details.components.AppReviews
|
||||||
|
import com.aurora.store.compose.ui.details.components.AppScreenshots
|
||||||
|
import com.aurora.store.compose.ui.details.components.AppTags
|
||||||
|
import com.aurora.store.compose.ui.details.components.AppTesting
|
||||||
import com.aurora.store.compose.ui.dev.DevProfileScreen
|
import com.aurora.store.compose.ui.dev.DevProfileScreen
|
||||||
import com.aurora.store.compose.ui.dialogs.ManualDownloadDialog
|
import com.aurora.store.compose.ui.dialogs.ManualDownloadDialog
|
||||||
import com.aurora.store.data.installer.AppInstaller
|
import com.aurora.store.data.installer.AppInstaller
|
||||||
import com.aurora.store.data.model.Report
|
import com.aurora.store.data.model.Report
|
||||||
import com.aurora.store.data.model.Scores
|
import com.aurora.store.data.model.Scores
|
||||||
import com.aurora.store.data.room.download.Download
|
import com.aurora.store.data.room.download.Download
|
||||||
import com.aurora.store.util.CommonUtil
|
|
||||||
import com.aurora.store.util.PackageUtil
|
import com.aurora.store.util.PackageUtil
|
||||||
import com.aurora.store.util.PackageUtil.PACKAGE_NAME_GMS
|
import com.aurora.store.util.PackageUtil.PACKAGE_NAME_GMS
|
||||||
import com.aurora.store.viewmodel.details.AppDetailsViewModel
|
import com.aurora.store.viewmodel.details.AppDetailsViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.random.Random
|
|
||||||
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
|
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -119,7 +85,6 @@ fun AppDetailsScreen(
|
|||||||
val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle()
|
val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle()
|
||||||
val download by viewModel.download.collectAsStateWithLifecycle()
|
val download by viewModel.download.collectAsStateWithLifecycle()
|
||||||
val installProgress by viewModel.installProgress.collectAsStateWithLifecycle()
|
val installProgress by viewModel.installProgress.collectAsStateWithLifecycle()
|
||||||
val suggestions by viewModel.suggestions.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
var shouldShowManualDownloadDialog by rememberSaveable { mutableStateOf(false) }
|
var shouldShowManualDownloadDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -158,7 +123,6 @@ fun AppDetailsScreen(
|
|||||||
ScreenContentApp(
|
ScreenContentApp(
|
||||||
app = this,
|
app = this,
|
||||||
isAnonymous = viewModel.authProvider.isAnonymous,
|
isAnonymous = viewModel.authProvider.isAnonymous,
|
||||||
suggestions = suggestions,
|
|
||||||
download = download,
|
download = download,
|
||||||
installProgress = installProgress,
|
installProgress = installProgress,
|
||||||
plexusScores = plexusScores,
|
plexusScores = plexusScores,
|
||||||
@@ -229,11 +193,10 @@ private fun ScreenContentError(onNavigateUp: () -> Unit = {}) {
|
|||||||
private fun ScreenContentApp(
|
private fun ScreenContentApp(
|
||||||
app: App,
|
app: App,
|
||||||
isAnonymous: Boolean = true,
|
isAnonymous: Boolean = true,
|
||||||
suggestions: List<App> = emptyList(),
|
|
||||||
download: Download? = null,
|
download: Download? = null,
|
||||||
installProgress: Float? = null,
|
installProgress: Float? = null,
|
||||||
plexusScores: Scores? = null,
|
plexusScores: Scores? = null,
|
||||||
dataSafetyReport: DataSafetyReport? = null,
|
dataSafetyReport: com.aurora.gplayapi.data.models.datasafety.Report? = null,
|
||||||
exodusReport: Report? = null,
|
exodusReport: Report? = null,
|
||||||
hasValidUpdate: Boolean = false,
|
hasValidUpdate: Boolean = false,
|
||||||
onNavigateUp: () -> Unit = {},
|
onNavigateUp: () -> Unit = {},
|
||||||
@@ -289,9 +252,9 @@ private fun ScreenContentApp(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
supportingPane = {
|
supportingPane = {
|
||||||
AnimatedPane(modifier = Modifier.safeContentPadding()) {
|
AnimatedPane {
|
||||||
ScreenContentAppSupportingPane(
|
DetailsSuggestions(
|
||||||
suggestions = suggestions,
|
onNavigateUp = null,
|
||||||
onNavigateToAppDetails = onNavigateToAppDetails
|
onNavigateToAppDetails = onNavigateToAppDetails
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -428,7 +391,7 @@ private fun ScreenContentAppMainPane(
|
|||||||
app = app,
|
app = app,
|
||||||
inProgress = isDownloading || isInstalling,
|
inProgress = isDownloading || isInstalling,
|
||||||
progress = progress,
|
progress = progress,
|
||||||
onNavigateToDetailsDevApps = onNavigateToDetailsDevProfile,
|
onNavigateToDetailsDevProfile = onNavigateToDetailsDevProfile,
|
||||||
hasValidUpdate = hasValidUpdate
|
hasValidUpdate = hasValidUpdate
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -443,7 +406,7 @@ private fun ScreenContentAppMainPane(
|
|||||||
)
|
)
|
||||||
|
|
||||||
AppScreenshots(
|
AppScreenshots(
|
||||||
list = app.screenshots,
|
screenshots = app.screenshots,
|
||||||
onNavigateToScreenshot = onNavigateToDetailsScreenshot
|
onNavigateToScreenshot = onNavigateToDetailsScreenshot
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -466,7 +429,7 @@ private fun ScreenContentAppMainPane(
|
|||||||
subtitle = stringResource(R.string.permissions, app.permissions.size)
|
subtitle = stringResource(R.string.permissions, app.permissions.size)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (dataSafetyReport != null) AppDataSafety(dataSafetyReport = dataSafetyReport)
|
if (dataSafetyReport != null) AppDataSafety(report = dataSafetyReport)
|
||||||
|
|
||||||
AppPrivacy(
|
AppPrivacy(
|
||||||
report = exodusReport,
|
report = exodusReport,
|
||||||
@@ -486,493 +449,6 @@ private fun ScreenContentAppMainPane(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,
|
|
||||||
progress: Float = 0F,
|
|
||||||
inProgress: Boolean = false,
|
|
||||||
onNavigateToDetailsDevApps: (developerName: String) -> Unit,
|
|
||||||
hasValidUpdate: Boolean = false,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
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(
|
|
||||||
text = app.displayName,
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = { onNavigateToDetailsDevApps(app.developerName) }),
|
|
||||||
text = app.developerName,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = if (!hasValidUpdate) {
|
|
||||||
stringResource(R.string.version, app.versionName, app.versionCode)
|
|
||||||
} else {
|
|
||||||
stringResource(
|
|
||||||
R.string.version_update,
|
|
||||||
PackageUtil.getInstalledVersionName(context, app.packageName),
|
|
||||||
PackageUtil.getInstalledVersionCode(context, app.packageName),
|
|
||||||
app.versionName,
|
|
||||||
app.versionCode
|
|
||||||
)
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.bodyVerySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display primary and secondary actions available for the app
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun AppActions(
|
|
||||||
primaryActionDisplayName: String,
|
|
||||||
secondaryActionDisplayName: String,
|
|
||||||
isPrimaryActionEnabled: Boolean = true,
|
|
||||||
isSecondaryActionEnabled: Boolean = true,
|
|
||||||
onPrimaryAction: () -> 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 = buttonWidthModifier,
|
|
||||||
onClick = onSecondaryAction,
|
|
||||||
enabled = isSecondaryActionEnabled
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = secondaryActionDisplayName,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
modifier = buttonWidthModifier,
|
|
||||||
onClick = onPrimaryAction,
|
|
||||||
enabled = isPrimaryActionEnabled
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = primaryActionDisplayName,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display app changelog
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun AppChangelog(changelog: String) {
|
|
||||||
HeaderComposable(title = stringResource(R.string.details_changelog))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small)))
|
|
||||||
.background(color = MaterialTheme.colorScheme.secondaryContainer)
|
|
||||||
.padding(dimensionResource(R.dimen.padding_medium))
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = if (changelog.isBlank()) {
|
|
||||||
AnnotatedString(text = stringResource(R.string.details_changelog_unavailable))
|
|
||||||
} else {
|
|
||||||
AnnotatedString.fromHtml(htmlString = changelog)
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display tags related to the app
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun AppTags(app: App) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val installsLabel = CommonUtil.addDiPrefix(app.installs)
|
|
||||||
val averageRating = if (app.labeledRating == "0.0" || app.labeledRating.isBlank()) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
app.labeledRating
|
|
||||||
}
|
|
||||||
val paidLabel = if (app.isFree) {
|
|
||||||
stringResource(R.string.details_free)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.details_paid)
|
|
||||||
}
|
|
||||||
val adsLabel = if (app.containsAds) {
|
|
||||||
stringResource(R.string.details_contains_ads)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.details_no_ads)
|
|
||||||
}
|
|
||||||
|
|
||||||
val tags = mapOf(
|
|
||||||
averageRating to R.drawable.ic_star,
|
|
||||||
installsLabel to R.drawable.ic_download_manager,
|
|
||||||
Formatter.formatShortFileSize(context, app.size) to R.drawable.ic_apk_install,
|
|
||||||
app.updatedOn to R.drawable.ic_updates,
|
|
||||||
paidLabel to R.drawable.ic_paid,
|
|
||||||
adsLabel to R.drawable.ic_campaign,
|
|
||||||
).filterKeys { it != null }
|
|
||||||
|
|
||||||
LazyRow(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
|
|
||||||
) {
|
|
||||||
items(items = tags.keys.toList()) { label ->
|
|
||||||
AppTagComposable(label = label!!, icon = tags.getValue(label))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display details of the app developer
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun AppDeveloperDetails(address: String, website: String, email: String) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
HeaderComposable(title = stringResource(R.string.details_dev_details))
|
|
||||||
Column {
|
|
||||||
if (website.isNotBlank()) {
|
|
||||||
InfoComposable(
|
|
||||||
title = AnnotatedString(text = stringResource(R.string.details_dev_website)),
|
|
||||||
description = AnnotatedString(text = website),
|
|
||||||
icon = R.drawable.ic_network,
|
|
||||||
onClick = { context.browse(website) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (email.isNotBlank()) {
|
|
||||||
InfoComposable(
|
|
||||||
title = AnnotatedString(text = stringResource(R.string.details_dev_email)),
|
|
||||||
description = AnnotatedString(text = email),
|
|
||||||
icon = R.drawable.ic_mail,
|
|
||||||
onClick = { context.mailTo(email) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (address.isNotBlank()) {
|
|
||||||
InfoComposable(
|
|
||||||
title = AnnotatedString(text = stringResource(R.string.details_dev_address)),
|
|
||||||
description = AnnotatedString.fromHtml(htmlString = address),
|
|
||||||
icon = R.drawable.ic_person_location,
|
|
||||||
onClick = { context.copyToClipBoard(address) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display screenshots of the app
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun AppScreenshots(list: List<Artwork>, onNavigateToScreenshot: (index: Int) -> Unit) {
|
|
||||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))) {
|
|
||||||
items(items = list, key = { artwork -> artwork.url }) { artwork ->
|
|
||||||
ScreenshotComposable(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(dimensionResource(R.dimen.screenshot_height))
|
|
||||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small)))
|
|
||||||
.clickable { onNavigateToScreenshot(list.indexOf(artwork)) },
|
|
||||||
url = "${artwork.url}=rw-w480-v1-e15"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display reviews of the app
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun AppReviews(rating: Rating, onNavigateToDetailsReview: () -> Unit) {
|
|
||||||
val stars = listOf(
|
|
||||||
rating.oneStar, rating.twoStar, rating.threeStar, rating.fourStar, rating.fiveStar
|
|
||||||
).map { it.toFloat() }.also {
|
|
||||||
// No ratings available, nothing to show
|
|
||||||
if (it.sum() == 0F) return
|
|
||||||
}
|
|
||||||
|
|
||||||
HeaderComposable(
|
|
||||||
title = stringResource(R.string.details_ratings),
|
|
||||||
onClick = onNavigateToDetailsReview
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium)),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = String.format(Locale.getDefault(), "%.1f", rating.average),
|
|
||||||
maxLines = 1,
|
|
||||||
style = MaterialTheme.typography.displayMedium,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = rating.abbreviatedLabel,
|
|
||||||
maxLines = 1,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium)),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
|
||||||
) {
|
|
||||||
stars.reversed().fastForEach { star ->
|
|
||||||
RatingComposable(
|
|
||||||
label = (stars.indexOf(star) + 1).toString(),
|
|
||||||
rating = star / stars.sum()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display app compatibility rating from Plexus
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun AppCompatibility(needsGms: Boolean, plexusScores: Scores?) {
|
|
||||||
HeaderComposable(
|
|
||||||
title = stringResource(R.string.details_compatibility_title),
|
|
||||||
subtitle = stringResource(R.string.plexus_powered),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!needsGms) {
|
|
||||||
InfoComposable(
|
|
||||||
icon = R.drawable.ic_menu_about,
|
|
||||||
title = AnnotatedString(
|
|
||||||
text = stringResource(R.string.details_compatibility_gms_not_required_title)
|
|
||||||
),
|
|
||||||
description = AnnotatedString(
|
|
||||||
text = stringResource(R.string.details_compatibility_gms_not_required_subtitle)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Nothing more to show
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
InfoComposable(
|
|
||||||
icon = R.drawable.ic_menu_about,
|
|
||||||
title = AnnotatedString(
|
|
||||||
text = stringResource(R.string.details_compatibility_gms_required_title)
|
|
||||||
),
|
|
||||||
description = AnnotatedString(
|
|
||||||
text = stringResource(R.string.details_compatibility_gms_required_subtitle)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val scoresStatus = mapOf(
|
|
||||||
R.string.details_compatibility_no_gms to plexusScores?.aosp?.status,
|
|
||||||
R.string.details_compatibility_microg to plexusScores?.microG?.status,
|
|
||||||
)
|
|
||||||
scoresStatus.forEach { (title, description) ->
|
|
||||||
InfoComposable(
|
|
||||||
icon = R.drawable.ic_android,
|
|
||||||
title = AnnotatedString(text = stringResource(title)),
|
|
||||||
description = AnnotatedString(
|
|
||||||
text = stringResource(description ?: R.string.details_compatibility_status_unknown)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display app's data safety report
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun AppDataSafety(dataSafetyReport: DataSafetyReport) {
|
|
||||||
HeaderComposable(
|
|
||||||
title = stringResource(R.string.details_data_safety_title),
|
|
||||||
subtitle = stringResource(R.string.details_data_safety_subtitle)
|
|
||||||
)
|
|
||||||
|
|
||||||
dataSafetyReport.entries.groupBy { it.type }.forEach { (type, entries) ->
|
|
||||||
when (type) {
|
|
||||||
EntryType.DATA_COLLECTED -> {
|
|
||||||
InfoComposable(
|
|
||||||
icon = R.drawable.ic_cloud_upload,
|
|
||||||
title = AnnotatedString(
|
|
||||||
text = stringResource(R.string.details_data_safety_collect)
|
|
||||||
),
|
|
||||||
description = AnnotatedString(
|
|
||||||
text = entries.first().subEntries.joinToString(", ") { it.name }
|
|
||||||
.ifBlank {
|
|
||||||
stringResource(R.string.details_data_safety_collect_none)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
EntryType.DATA_SHARED -> {
|
|
||||||
InfoComposable(
|
|
||||||
icon = R.drawable.ic_share,
|
|
||||||
title = AnnotatedString(
|
|
||||||
text = stringResource(R.string.details_data_safety_shared)
|
|
||||||
),
|
|
||||||
description = AnnotatedString(
|
|
||||||
text = entries.first().subEntries.joinToString(", ") { it.name }
|
|
||||||
.ifBlank {
|
|
||||||
stringResource(R.string.details_data_safety_share_none)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't care about any other sections
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable to display app's privacy report from ExodusPrivacy
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun AppPrivacy(report: Report?, onNavigateToDetailsExodus: (() -> Unit)?) {
|
|
||||||
HeaderComposable(
|
|
||||||
title = stringResource(R.string.details_privacy),
|
|
||||||
subtitle = stringResource(R.string.exodus_powered),
|
|
||||||
onClick = onNavigateToDetailsExodus
|
|
||||||
)
|
|
||||||
|
|
||||||
val reportStatus = when {
|
|
||||||
report == null -> stringResource(R.string.failed_to_fetch_report)
|
|
||||||
report.id == -1 -> stringResource(R.string.exodus_progress)
|
|
||||||
else -> if (report.trackers.isEmpty()) {
|
|
||||||
stringResource(R.string.exodus_no_tracker)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.exodus_report_trackers, report.trackers.size, report.version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InfoComposable(
|
|
||||||
icon = R.drawable.ic_visibility,
|
|
||||||
title = AnnotatedString(text = reportStatus),
|
|
||||||
description = AnnotatedString(text = stringResource(R.string.exodus_tracker_desc))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
@PreviewScreenSizes
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalCoilApi::class)
|
@OptIn(ExperimentalCoilApi::class)
|
||||||
@@ -981,7 +457,6 @@ private fun AppDetailsScreenPreview(@PreviewParameter(AppPreviewProvider::class)
|
|||||||
ScreenContentApp(
|
ScreenContentApp(
|
||||||
app = app,
|
app = app,
|
||||||
isAnonymous = false,
|
isAnonymous = false,
|
||||||
suggestions = List(5) { app.copy(id = Random.nextInt()) },
|
|
||||||
hasValidUpdate = false
|
hasValidUpdate = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.details
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
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.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
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 coil3.annotation.ExperimentalCoilApi
|
||||||
|
import coil3.compose.LocalAsyncImagePreviewHandler
|
||||||
|
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.TopAppBarComposable
|
||||||
|
import com.aurora.store.compose.composables.app.AppListComposable
|
||||||
|
import com.aurora.store.compose.composables.preview.AppPreviewProvider
|
||||||
|
import com.aurora.store.compose.composables.preview.coilPreviewProvider
|
||||||
|
import com.aurora.store.viewmodel.details.AppDetailsViewModel
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Screen to display similar and related suggestions for an app, supposed to be used internally
|
||||||
|
* by the [AppDetailsScreen] and not navigable from other screens
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DetailsSuggestions(
|
||||||
|
onNavigateUp: (() -> Unit)?,
|
||||||
|
viewModel: AppDetailsViewModel = hiltViewModel(),
|
||||||
|
onNavigateToAppDetails: (packageName: String) -> Unit
|
||||||
|
) {
|
||||||
|
val suggestions by viewModel.suggestions.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
ScreenContent(
|
||||||
|
onNavigateUp = onNavigateUp,
|
||||||
|
suggestions = suggestions,
|
||||||
|
onNavigateToAppDetails = onNavigateToAppDetails
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ScreenContent(
|
||||||
|
onNavigateUp: (() -> Unit)? = {},
|
||||||
|
suggestions: List<App> = emptyList(),
|
||||||
|
onNavigateToAppDetails: (packageName: String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = { TopAppBarComposable(onNavigateUp = onNavigateUp) }
|
||||||
|
) { 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) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalCoilApi::class)
|
||||||
|
private fun DetailsSuggestionsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
|
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) {
|
||||||
|
ScreenContent(
|
||||||
|
onNavigateUp = null,
|
||||||
|
suggestions = List(10) { app.copy(id = Random.nextInt()) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.details.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
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.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 androidx.window.core.layout.WindowWidthSizeClass
|
||||||
|
import com.aurora.store.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display primary and secondary actions available for the app, supposed to be used
|
||||||
|
* as a part of the Column with proper vertical arrangement spacing in the AppDetailsScreen.
|
||||||
|
* @param primaryActionDisplayName Name of the primary action
|
||||||
|
* @param secondaryActionDisplayName Name of the secondary action
|
||||||
|
* @param isPrimaryActionEnabled Whether the primary action is enabled
|
||||||
|
* @param isSecondaryActionEnabled Whether the secondary action is enabled
|
||||||
|
* @param onPrimaryAction Callback when the primary action is clicked
|
||||||
|
* @param onSecondaryAction Callback when the secondary action is clicked
|
||||||
|
* @param windowAdaptiveInfo Adaptive window information
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppActions(
|
||||||
|
primaryActionDisplayName: String,
|
||||||
|
secondaryActionDisplayName: String,
|
||||||
|
isPrimaryActionEnabled: Boolean = true,
|
||||||
|
isSecondaryActionEnabled: Boolean = true,
|
||||||
|
onPrimaryAction: () -> 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 = buttonWidthModifier,
|
||||||
|
onClick = onSecondaryAction,
|
||||||
|
enabled = isSecondaryActionEnabled
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = secondaryActionDisplayName,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = buttonWidthModifier,
|
||||||
|
onClick = onPrimaryAction,
|
||||||
|
enabled = isPrimaryActionEnabled
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = primaryActionDisplayName,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun AppActionsPreview() {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))) {
|
||||||
|
AppActions(
|
||||||
|
primaryActionDisplayName = stringResource(R.string.action_install),
|
||||||
|
secondaryActionDisplayName = stringResource(R.string.title_manual_download)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.details.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.fromHtml
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
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.preview.AppPreviewProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display app changelog, supposed to be used as a part
|
||||||
|
* of the Column with proper vertical arrangement spacing in the AppDetailsScreen.
|
||||||
|
* @param changelog Changelog to show
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppChangelog(changelog: String) {
|
||||||
|
HeaderComposable(title = stringResource(R.string.details_changelog))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small)))
|
||||||
|
.background(color = MaterialTheme.colorScheme.secondaryContainer)
|
||||||
|
.padding(dimensionResource(R.dimen.padding_medium))
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (changelog.isBlank()) {
|
||||||
|
AnnotatedString(text = stringResource(R.string.details_changelog_unavailable))
|
||||||
|
} else {
|
||||||
|
AnnotatedString.fromHtml(htmlString = changelog)
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun AppChangelogPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))) {
|
||||||
|
AppChangelog(changelog = app.changes)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.details.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composables.HeaderComposable
|
||||||
|
import com.aurora.store.compose.composables.InfoComposable
|
||||||
|
import com.aurora.store.data.model.Scores
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display app compatibility rating from Plexus, supposed to be used as a part
|
||||||
|
* of the Column with proper vertical arrangement spacing in the AppDetailsScreen.
|
||||||
|
* @param needsGms Whether this app needs Google Play Services to work properly
|
||||||
|
* @param plexusScores Scores from the Plexus
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppCompatibility(needsGms: Boolean, plexusScores: Scores? = null) {
|
||||||
|
HeaderComposable(
|
||||||
|
title = stringResource(R.string.details_compatibility_title),
|
||||||
|
subtitle = stringResource(R.string.plexus_powered),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!needsGms) {
|
||||||
|
InfoComposable(
|
||||||
|
icon = R.drawable.ic_menu_about,
|
||||||
|
title = AnnotatedString(
|
||||||
|
text = stringResource(R.string.details_compatibility_gms_not_required_title)
|
||||||
|
),
|
||||||
|
description = AnnotatedString(
|
||||||
|
text = stringResource(R.string.details_compatibility_gms_not_required_subtitle)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Nothing more to show
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoComposable(
|
||||||
|
icon = R.drawable.ic_menu_about,
|
||||||
|
title = AnnotatedString(
|
||||||
|
text = stringResource(R.string.details_compatibility_gms_required_title)
|
||||||
|
),
|
||||||
|
description = AnnotatedString(
|
||||||
|
text = stringResource(R.string.details_compatibility_gms_required_subtitle)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val scoresStatus = mapOf(
|
||||||
|
R.string.details_compatibility_no_gms to plexusScores?.aosp?.status,
|
||||||
|
R.string.details_compatibility_microg to plexusScores?.microG?.status,
|
||||||
|
)
|
||||||
|
scoresStatus.forEach { (title, description) ->
|
||||||
|
InfoComposable(
|
||||||
|
icon = R.drawable.ic_android,
|
||||||
|
title = AnnotatedString(text = stringResource(title)),
|
||||||
|
description = AnnotatedString(
|
||||||
|
text = stringResource(description ?: R.string.details_compatibility_status_unknown)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun AppCompatibilityPreview() {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))) {
|
||||||
|
AppCompatibility(needsGms = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.details.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.gplayapi.data.models.datasafety.Entry
|
||||||
|
import com.aurora.gplayapi.data.models.datasafety.EntryType
|
||||||
|
import com.aurora.gplayapi.data.models.datasafety.Report
|
||||||
|
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.preview.AppPreviewProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display app's data safety report, supposed to be used as a part
|
||||||
|
* of the Column with proper vertical arrangement spacing in the AppDetailsScreen.
|
||||||
|
* @param report App's data safety report
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppDataSafety(report: Report) {
|
||||||
|
HeaderComposable(
|
||||||
|
title = stringResource(R.string.details_data_safety_title),
|
||||||
|
subtitle = stringResource(R.string.details_data_safety_subtitle)
|
||||||
|
)
|
||||||
|
|
||||||
|
report.entries.groupBy { it.type }.forEach { (type, entries) ->
|
||||||
|
when (type) {
|
||||||
|
EntryType.DATA_COLLECTED -> {
|
||||||
|
InfoComposable(
|
||||||
|
icon = R.drawable.ic_cloud_upload,
|
||||||
|
title = AnnotatedString(
|
||||||
|
text = stringResource(R.string.details_data_safety_collect)
|
||||||
|
),
|
||||||
|
description = AnnotatedString(
|
||||||
|
text = entries.first().subEntries.joinToString(", ") { it.name }
|
||||||
|
.ifBlank {
|
||||||
|
stringResource(R.string.details_data_safety_collect_none)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
EntryType.DATA_SHARED -> {
|
||||||
|
InfoComposable(
|
||||||
|
icon = R.drawable.ic_share,
|
||||||
|
title = AnnotatedString(
|
||||||
|
text = stringResource(R.string.details_data_safety_shared)
|
||||||
|
),
|
||||||
|
description = AnnotatedString(
|
||||||
|
text = entries.first().subEntries.joinToString(", ") { it.name }
|
||||||
|
.ifBlank {
|
||||||
|
stringResource(R.string.details_data_safety_share_none)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't care about any other sections
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun AppDataSafetyPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))) {
|
||||||
|
AppDataSafety(
|
||||||
|
report = Report(
|
||||||
|
packageName = app.packageName,
|
||||||
|
entries = listOf(
|
||||||
|
Entry(type = EntryType.DATA_COLLECTED, name = String(), description = String()),
|
||||||
|
Entry(type = EntryType.DATA_SHARED, name = String(), description = String())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.details.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import coil3.annotation.ExperimentalCoilApi
|
||||||
|
import coil3.compose.LocalAsyncImagePreviewHandler
|
||||||
|
import com.aurora.extensions.bodyVerySmall
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composables.app.AnimatedAppIconComposable
|
||||||
|
import com.aurora.store.compose.composables.preview.AppPreviewProvider
|
||||||
|
import com.aurora.store.compose.composables.preview.coilPreviewProvider
|
||||||
|
import com.aurora.store.util.PackageUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display basic app details, supposed to be used as a part
|
||||||
|
* of the Column with proper vertical arrangement spacing in the AppDetailsScreen.
|
||||||
|
* @param app App to show details about
|
||||||
|
* @param progress Ongoing progress percentage out of 100, for e.g. 50.0
|
||||||
|
* @param inProgress Whether there is some ongoing progress related to the app
|
||||||
|
* @param onNavigateToDetailsDevProfile Callback when the developer name is tapped
|
||||||
|
* @param hasValidUpdate Whether the app has a valid update available
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppDetails(
|
||||||
|
app: App,
|
||||||
|
progress: Float = 0F,
|
||||||
|
inProgress: Boolean = false,
|
||||||
|
onNavigateToDetailsDevProfile: (developerName: String) -> Unit = {},
|
||||||
|
hasValidUpdate: Boolean = false,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
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(
|
||||||
|
text = app.displayName,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onNavigateToDetailsDevProfile(app.developerName) }),
|
||||||
|
text = app.developerName,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (!hasValidUpdate) {
|
||||||
|
stringResource(R.string.version, app.versionName, app.versionCode)
|
||||||
|
} else {
|
||||||
|
stringResource(
|
||||||
|
R.string.version_update,
|
||||||
|
PackageUtil.getInstalledVersionName(context, app.packageName),
|
||||||
|
PackageUtil.getInstalledVersionCode(context, app.packageName),
|
||||||
|
app.versionName,
|
||||||
|
app.versionCode
|
||||||
|
)
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodyVerySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalCoilApi::class)
|
||||||
|
private fun AppDetailsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))) {
|
||||||
|
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) {
|
||||||
|
AppDetails(app = app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.details.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.fromHtml
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import com.aurora.extensions.browse
|
||||||
|
import com.aurora.extensions.copyToClipBoard
|
||||||
|
import com.aurora.extensions.mailTo
|
||||||
|
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.preview.AppPreviewProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display details of the app developer, supposed to be used as a part
|
||||||
|
* of the Column with proper vertical arrangement spacing in the AppDetailsScreen.
|
||||||
|
* @param address Address of the app developer
|
||||||
|
* @param website Website of the app developer
|
||||||
|
* @param email Email address of the app developer
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppDeveloperDetails(address: String, website: String, email: String) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
HeaderComposable(title = stringResource(R.string.details_dev_details))
|
||||||
|
Column {
|
||||||
|
if (website.isNotBlank()) {
|
||||||
|
InfoComposable(
|
||||||
|
title = AnnotatedString(text = stringResource(R.string.details_dev_website)),
|
||||||
|
description = AnnotatedString(text = website),
|
||||||
|
icon = R.drawable.ic_network,
|
||||||
|
onClick = { context.browse(website) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email.isNotBlank()) {
|
||||||
|
InfoComposable(
|
||||||
|
title = AnnotatedString(text = stringResource(R.string.details_dev_email)),
|
||||||
|
description = AnnotatedString(text = email),
|
||||||
|
icon = R.drawable.ic_mail,
|
||||||
|
onClick = { context.mailTo(email) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address.isNotBlank()) {
|
||||||
|
InfoComposable(
|
||||||
|
title = AnnotatedString(text = stringResource(R.string.details_dev_address)),
|
||||||
|
description = AnnotatedString.fromHtml(htmlString = address),
|
||||||
|
icon = R.drawable.ic_person_location,
|
||||||
|
onClick = { context.copyToClipBoard(address) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun AppDeveloperDetailsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))) {
|
||||||
|
AppDeveloperDetails(
|
||||||
|
address = app.developerAddress,
|
||||||
|
website = app.developerWebsite,
|
||||||
|
email = app.developerEmail
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.details.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composables.HeaderComposable
|
||||||
|
import com.aurora.store.compose.composables.InfoComposable
|
||||||
|
import com.aurora.store.data.model.Report
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display app's privacy report from ExodusPrivacy, supposed to be used as a part
|
||||||
|
* of the Column with proper vertical arrangement spacing in the AppDetailsScreen.
|
||||||
|
* @param report Report from Exodus Privacy for the app
|
||||||
|
* @param onNavigateToDetailsExodus Callback when the user navigates
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppPrivacy(report: Report?, onNavigateToDetailsExodus: (() -> Unit)? = null) {
|
||||||
|
HeaderComposable(
|
||||||
|
title = stringResource(R.string.details_privacy),
|
||||||
|
subtitle = stringResource(R.string.exodus_powered),
|
||||||
|
onClick = onNavigateToDetailsExodus
|
||||||
|
)
|
||||||
|
|
||||||
|
val reportStatus = when {
|
||||||
|
report == null -> stringResource(R.string.failed_to_fetch_report)
|
||||||
|
report.id == -1 -> stringResource(R.string.exodus_progress)
|
||||||
|
else -> if (report.trackers.isEmpty()) {
|
||||||
|
stringResource(R.string.exodus_no_tracker)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.exodus_report_trackers, report.trackers.size, report.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoComposable(
|
||||||
|
icon = R.drawable.ic_visibility,
|
||||||
|
title = AnnotatedString(text = reportStatus),
|
||||||
|
description = AnnotatedString(text = stringResource(R.string.exodus_tracker_desc))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun AppPrivacyPreview() {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))) {
|
||||||
|
AppPrivacy(report = Report(), onNavigateToDetailsExodus = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.details.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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.Alignment
|
||||||
|
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 androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.gplayapi.data.models.Rating
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composables.HeaderComposable
|
||||||
|
import com.aurora.store.compose.composables.details.RatingComposable
|
||||||
|
import com.aurora.store.compose.composables.preview.AppPreviewProvider
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display reviews of the app, supposed to be used as a part
|
||||||
|
* of the Column with proper vertical arrangement spacing in the AppDetailsScreen.
|
||||||
|
* @param rating Rating of the app
|
||||||
|
* @param onNavigateToDetailsReview Callback when the user navigates
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppReviews(rating: Rating, onNavigateToDetailsReview: () -> Unit = {}) {
|
||||||
|
val stars = listOf(
|
||||||
|
rating.oneStar, rating.twoStar, rating.threeStar, rating.fourStar, rating.fiveStar
|
||||||
|
).map { it.toFloat() }.also {
|
||||||
|
// No ratings available, nothing to show
|
||||||
|
if (it.sum() == 0F) return
|
||||||
|
}
|
||||||
|
|
||||||
|
HeaderComposable(
|
||||||
|
title = stringResource(R.string.details_ratings),
|
||||||
|
onClick = onNavigateToDetailsReview
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium)),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = String.format(Locale.getDefault(), "%.1f", rating.average),
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.displayMedium,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = rating.abbreviatedLabel,
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium)),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
|
||||||
|
) {
|
||||||
|
stars.reversed().fastForEach { star ->
|
||||||
|
RatingComposable(
|
||||||
|
label = (stars.indexOf(star) + 1).toString(),
|
||||||
|
rating = star / stars.sum()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun AppReviewsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))) {
|
||||||
|
AppReviews(rating = app.rating)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.details.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import coil3.annotation.ExperimentalCoilApi
|
||||||
|
import coil3.compose.LocalAsyncImagePreviewHandler
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.gplayapi.data.models.Artwork
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composables.details.ScreenshotComposable
|
||||||
|
import com.aurora.store.compose.composables.preview.AppPreviewProvider
|
||||||
|
import com.aurora.store.compose.composables.preview.coilPreviewProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display screenshots of the app, supposed to be used as a part
|
||||||
|
* of the Column with proper vertical arrangement spacing in the AppDetailsScreen.
|
||||||
|
* @param screenshots Screenshots to display
|
||||||
|
* @param onNavigateToScreenshot Callback when a screenshot is clicked
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppScreenshots(screenshots: List<Artwork>, onNavigateToScreenshot: (index: Int) -> Unit = {}) {
|
||||||
|
LazyRow(horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))) {
|
||||||
|
items(items = screenshots, key = { artwork -> artwork.url }) { artwork ->
|
||||||
|
ScreenshotComposable(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(dimensionResource(R.dimen.screenshot_height))
|
||||||
|
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small)))
|
||||||
|
.clickable { onNavigateToScreenshot(screenshots.indexOf(artwork)) },
|
||||||
|
url = "${artwork.url}=rw-w480-v1-e15"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalCoilApi::class)
|
||||||
|
private fun AppScreenshotsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))) {
|
||||||
|
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) {
|
||||||
|
AppScreenshots(screenshots = app.screenshots)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.details.components
|
||||||
|
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composables.app.AppTagComposable
|
||||||
|
import com.aurora.store.compose.composables.preview.AppPreviewProvider
|
||||||
|
import com.aurora.store.util.CommonUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display tags related to the app, supposed to be used as a part
|
||||||
|
* of the Column with proper vertical arrangement spacing in the AppDetailsScreen.
|
||||||
|
* @param app App to show tags
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppTags(app: App) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val installsLabel = CommonUtil.addDiPrefix(app.installs)
|
||||||
|
val averageRating = if (app.labeledRating == "0.0" || app.labeledRating.isBlank()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
app.labeledRating
|
||||||
|
}
|
||||||
|
val paidLabel = if (app.isFree) {
|
||||||
|
stringResource(R.string.details_free)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.details_paid)
|
||||||
|
}
|
||||||
|
val adsLabel = if (app.containsAds) {
|
||||||
|
stringResource(R.string.details_contains_ads)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.details_no_ads)
|
||||||
|
}
|
||||||
|
|
||||||
|
val tags = mapOf(
|
||||||
|
averageRating to R.drawable.ic_star,
|
||||||
|
installsLabel to R.drawable.ic_download_manager,
|
||||||
|
Formatter.formatShortFileSize(context, app.size) to R.drawable.ic_apk_install,
|
||||||
|
app.updatedOn to R.drawable.ic_updates,
|
||||||
|
paidLabel to R.drawable.ic_paid,
|
||||||
|
adsLabel to R.drawable.ic_campaign,
|
||||||
|
).filterKeys { it != null }
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
|
||||||
|
) {
|
||||||
|
items(items = tags.keys.toList()) { label ->
|
||||||
|
AppTagComposable(label = label!!, icon = tags.getValue(label))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun AppTagsPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))) {
|
||||||
|
AppTags(app = app)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.details.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
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.preview.AppPreviewProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to display app's beta testing status, supposed to be used as a part of the
|
||||||
|
* Column with proper vertical arrangement spacing in the AppDetailsScreen.
|
||||||
|
*
|
||||||
|
* @param isSubscribed Whether the user is subscribed to the beta testing program
|
||||||
|
* @param onTestingSubscriptionChange Callback when the the subscription button is clicked
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppTesting(
|
||||||
|
isSubscribed: Boolean,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun AppTestingPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))) {
|
||||||
|
AppTesting(isSubscribed = app.testingProgram!!.isSubscribed)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user