Merge branch 'dev' into 'master'

dev

See merge request AuroraOSS/AuroraStore!558
This commit is contained in:
Rahul Patel
2026-05-08 02:12:55 +05:30
36 changed files with 805 additions and 343 deletions

View File

@@ -11,14 +11,12 @@ 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.shape.RoundedCornerShape
import androidx.compose.material3.Icon
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.draw.clip
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -46,18 +44,16 @@ fun Header(
Row(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small)))
.clickable(onClick = { if (onClick != null) onClick() }, enabled = onClick != null)
.padding(
horizontal = dimensionResource(R.dimen.padding_small),
vertical = dimensionResource(R.dimen.padding_xxsmall)
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_xsmall)
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1F),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_xsmall))
modifier = Modifier.weight(1F)
) {
Text(
text = title,
@@ -68,7 +64,8 @@ fun Header(
if (!subtitle.isNullOrBlank()) {
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}

View File

@@ -56,7 +56,7 @@ fun Info(
.fillMaxWidth()
.clickable(onClick = { if (onClick != null) onClick() }, enabled = onClick != null)
.padding(
horizontal = dimensionResource(R.dimen.padding_small),
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_xxsmall)
),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_normal)),
@@ -75,7 +75,7 @@ fun Info(
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.secondary
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}

View File

@@ -5,6 +5,9 @@
package com.aurora.store.compose.theme
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialExpressiveTheme
@@ -14,8 +17,11 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.colorResource
import androidx.core.view.WindowCompat
import com.aurora.store.R
import com.aurora.store.util.Preferences
@@ -46,5 +52,41 @@ fun AuroraTheme(content: @Composable () -> Unit) {
else -> if (isSystemInDarkTheme()) darkScheme else lightScheme
}
val darkTheme = when (themeStyle) {
1 -> false
2 -> true
else -> isSystemInDarkTheme()
}
/**
* Side effect to update the system bars to be transparent and match the theme's light/dark mode.
* This is necessary on OEM devices that don't properly support dynamic theming and may have issues with light/dark status bar icons.
*/
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val activity = view.context.findActivity() ?: return@SideEffect
val window = activity.window
// Transparent system bars
window.statusBarColor = android.graphics.Color.TRANSPARENT
window.navigationBarColor = android.graphics.Color.TRANSPARENT
// Control icon colors explicitly
WindowCompat
.getInsetsController(window, view)
.apply {
isAppearanceLightStatusBars = !darkTheme
isAppearanceLightNavigationBars = !darkTheme
}
}
}
MaterialExpressiveTheme(colorScheme = colorScheme, content = content)
}
tailrec fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}

View File

@@ -5,23 +5,31 @@
package com.aurora.store.compose.ui.blacklist
import android.content.pm.PackageInfo
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.material3.AppBarWithSearch
import androidx.compose.material3.ExpandedFullScreenSearchBar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberSearchBarState
import androidx.compose.runtime.Composable
@@ -30,9 +38,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
@@ -40,19 +50,23 @@ 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.PreviewWrapper
import androidx.core.content.pm.PackageInfoCompat
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.Constants
import com.aurora.extensions.toast
import com.aurora.store.R
import com.aurora.store.compose.composable.BlackListItem
import com.aurora.store.compose.composable.ContainedLoadingIndicator
import com.aurora.store.compose.composable.ScrollHint
import com.aurora.store.compose.composable.TextDividerComposable
import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.compose.ui.blacklist.menu.BlacklistMenu
import com.aurora.store.compose.ui.blacklist.menu.MenuItem
import com.aurora.store.util.PackageUtil
import com.aurora.store.data.model.BlacklistAppItem
import com.aurora.store.viewmodel.blacklist.BlacklistViewModel
import java.util.Calendar
import kotlinx.coroutines.android.awaitFrame
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@@ -65,7 +79,6 @@ fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hi
packages = packages,
onNavigateUp = onNavigateUp,
isPackageBlacklisted = { pkgName -> pkgName in viewModel.blacklist },
isPackageFiltered = { pkgInfo -> viewModel.isFiltered(pkgInfo) },
onBlacklistImport = { uri ->
viewModel.importBlacklist(uri)
context.toast(R.string.toast_black_import_success)
@@ -84,10 +97,9 @@ fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hi
@Composable
private fun ScreenContent(
packages: List<PackageInfo>? = null,
packages: List<BlacklistAppItem>? = null,
onNavigateUp: () -> Unit = {},
isPackageBlacklisted: (packageName: String) -> Boolean = { false },
isPackageFiltered: (packageInfo: PackageInfo) -> Boolean = { false },
onBlacklistImport: (uri: Uri) -> Unit = {},
onBlacklistExport: (uri: Uri) -> Unit = {},
onBlacklist: (packageName: String) -> Unit = {},
@@ -112,6 +124,7 @@ private fun ScreenContent(
}
}
)
val docExportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE),
onResult = {
@@ -157,11 +170,23 @@ private fun ScreenContent(
@Composable
fun SearchBar() {
val interactionSource = remember { MutableInteractionSource() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collectLatest { interaction ->
if (interaction is PressInteraction.Press) {
awaitFrame()
focusRequester.requestFocus()
}
}
}
val inputField = @Composable {
SearchBarDefaults.InputField(
modifier = Modifier.focusRequester(focusRequester),
searchBarState = searchBarState,
textFieldState = textFieldState,
interactionSource = interactionSource,
onSearch = { query -> onRequestSearch(query) },
placeholder = {
Text(
@@ -205,37 +230,110 @@ private fun ScreenContent(
)
}
},
actions = { if (!packages.isNullOrEmpty()) SetupMenu() }
actions = { SetupMenu() },
colors = SearchBarDefaults.appBarWithSearchColors(
appBarContainerColor = Color.Transparent
)
)
ExpandedFullScreenSearchBar(state = searchBarState, inputField = inputField) {
if (textFieldState.text.length >= 3) {
packages?.take(10)?.forEach { pkg ->
Text(
text = pkg.displayName,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.clickable { onRequestSearch(pkg.displayName) }
.padding(
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_small)
)
)
}
}
}
}
Scaffold(topBar = { SearchBar() }) { paddingValues ->
LazyColumn(
Scaffold(
topBar = { SearchBar() }
) { paddingValues ->
val listState = rememberLazyListState()
Box(
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
.fillMaxSize()
.padding(paddingValues)
) {
items(items = packages ?: emptyList(), key = { p -> p.packageName.hashCode() }) { pkg ->
val isBlacklisted = isPackageBlacklisted(pkg.packageName)
val isFiltered = isPackageFiltered(pkg)
BlackListItem(
icon = PackageUtil.getIconForPackage(context, pkg.packageName)!!,
displayName = pkg.applicationInfo!!.loadLabel(
context.packageManager
).toString(),
packageName = pkg.packageName,
versionName = pkg.versionName!!,
versionCode = PackageInfoCompat.getLongVersionCode(pkg),
isChecked = isBlacklisted || isFiltered,
isEnabled = !isFiltered,
onClick = {
if (isBlacklisted) {
onWhitelist(pkg.packageName)
} else {
onBlacklist(pkg.packageName)
if (packages == null) {
ContainedLoadingIndicator()
} else {
val (selectedPackages, otherPackages) = packages.partition { pkg ->
isPackageBlacklisted(pkg.packageName) || pkg.isFiltered
}
@Composable
fun BlacklistRow(pkg: BlacklistAppItem) {
val isBlacklisted = isPackageBlacklisted(pkg.packageName)
BlackListItem(
icon = pkg.icon,
displayName = pkg.displayName,
packageName = pkg.packageName,
versionName = pkg.versionName,
versionCode = pkg.versionCode,
isChecked = isBlacklisted || pkg.isFiltered,
isEnabled = !pkg.isFiltered,
onClick = {
if (isBlacklisted) {
onWhitelist(pkg.packageName)
} else {
onBlacklist(pkg.packageName)
}
}
)
}
LazyColumn(
modifier = Modifier.fillMaxWidth(),
state = listState,
verticalArrangement = Arrangement.spacedBy(
dimensionResource(R.dimen.margin_xxsmall)
)
) {
if (selectedPackages.isNotEmpty()) {
stickyHeader(key = "header_selected") {
Surface(modifier = Modifier.fillMaxWidth()) {
TextDividerComposable(
title = stringResource(
R.string.header_blacklist_selected
)
)
}
}
items(items = selectedPackages, key = { p ->
p.packageName.hashCode()
}) { pkg -> BlacklistRow(pkg) }
}
if (otherPackages.isNotEmpty()) {
stickyHeader(key = "header_others") {
Surface(modifier = Modifier.fillMaxWidth()) {
TextDividerComposable(
title = stringResource(
R.string.header_blacklist_others
)
)
}
}
items(items = otherPackages, key = { p ->
p.packageName.hashCode()
}) { pkg -> BlacklistRow(pkg) }
}
}
ScrollHint(
listState = listState,
bottomPadding = 5.dp,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}

View File

@@ -12,14 +12,14 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
@@ -49,9 +49,12 @@ 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.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.NavKey
import com.aurora.Constants
import com.aurora.extensions.appInfo
import com.aurora.extensions.requiresGMS
import com.aurora.extensions.requiresObbDir
@@ -65,6 +68,7 @@ import com.aurora.store.R
import com.aurora.store.compose.composable.ContainedLoadingIndicator
import com.aurora.store.compose.composable.Error
import com.aurora.store.compose.composable.Header
import com.aurora.store.compose.composable.ScrollHint
import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.composable.app.LargeAppListItem
import com.aurora.store.compose.navigation.Screen
@@ -163,7 +167,7 @@ fun AppDetailsScreen(
PackageUtil.getLaunchIntent(context, packageName)
)
} catch (_: ActivityNotFoundException) {
context.toast(context.getString(R.string.unable_to_open))
context.toast(R.string.unable_to_open)
}
},
onTestingSubscriptionChange = { subscribe ->
@@ -313,6 +317,21 @@ private fun ScreenContentApp(
MenuItem.ADD_TO_HOME -> {
ShortcutManagerUtil.requestPinShortcut(context, app.packageName)
}
MenuItem.PLAY_STORE -> {
val uri = "${Constants.SHARE_URL}${app.packageName}".toUri()
val intent = Intent(Intent.ACTION_VIEW).apply { data = uri }
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(
intent.apply {
setPackage(Constants.PACKAGE_NAME_PLAY_STORE)
}
)
} else {
context.startActivity(intent)
}
}
}
}
}
@@ -397,81 +416,126 @@ private fun ScreenContentApp(
)
}
) { paddingValues ->
Column(
val listState = rememberLazyListState()
Box(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
.padding(paddingValues)
) {
Details(
app = app,
state = state,
onNavigateToDetailsDevProfile = { showExtraPane(Screen.DevProfile(it)) }
)
SetupActions()
Tags(app = app)
Changelog(changelog = app.changes)
Header(
title = stringResource(R.string.details_more_about_app),
subtitle = app.shortDescription,
onClick = { showExtraPane(ExtraScreen.More) }
)
Screenshots(
screenshots = app.screenshots,
onNavigateToScreenshot = { showExtraPane(ExtraScreen.Screenshot(it)) }
)
RatingAndReviews(
rating = app.rating,
featuredReviews = featuredReviews,
onNavigateToDetailsReview = { showExtraPane(ExtraScreen.Review) }
)
if (!isAnonymous && app.testingProgram?.isAvailable == true) {
Testing(
isSubscribed = app.testingProgram!!.isSubscribed,
onTestingSubscriptionChange = onTestingSubscriptionChange
)
}
Compatibility(needsGms = app.requiresGMS(), plexusScores = plexusScores)
Header(
title = stringResource(R.string.details_permission),
subtitle = if (app.permissions.isNotEmpty()) {
stringResource(R.string.permissions_requested, app.permissions.size)
} else {
stringResource(R.string.details_no_permission)
},
onClick = if (app.permissions.isNotEmpty()) {
{ showExtraPane(ExtraScreen.Permission) }
} else {
null
LazyColumn(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(
dimensionResource(R.dimen.margin_medium)
),
state = listState
) {
item {
Details(
app = app,
state = state,
onNavigateToDetailsDevProfile = { showExtraPane(Screen.DevProfile(it)) }
)
}
)
if (dataSafetyReport != null) {
DataSafety(report = dataSafetyReport, privacyPolicyUrl = app.privacyPolicyUrl)
}
Privacy(
report = exodusReport,
onNavigateToDetailsExodus = if (exodusReport != null && exodusReport.id != -1) {
{ showExtraPane(ExtraScreen.Exodus) }
} else {
null
item {
SetupActions()
}
)
DeveloperDetails(
address = app.developerAddress,
website = app.developerWebsite,
email = app.developerEmail
item {
Tags(app = app)
}
item {
Changelog(changelog = app.changes)
}
item {
Header(
title = stringResource(R.string.details_more_about_app),
subtitle = app.shortDescription,
onClick = { showExtraPane(ExtraScreen.More) }
)
}
item {
Screenshots(
screenshots = app.screenshots,
onNavigateToScreenshot = { showExtraPane(ExtraScreen.Screenshot(it)) }
)
}
item {
RatingAndReviews(
rating = app.rating,
featuredReviews = featuredReviews,
onNavigateToDetailsReview = { showExtraPane(ExtraScreen.Review) }
)
}
item {
if (!isAnonymous && app.testingProgram?.isAvailable == true) {
Testing(
isSubscribed = app.testingProgram!!.isSubscribed,
onTestingSubscriptionChange = onTestingSubscriptionChange
)
}
}
item {
Compatibility(needsGms = app.requiresGMS(), plexusScores = plexusScores)
}
item {
Header(
title = stringResource(R.string.details_permission),
subtitle = if (app.permissions.isNotEmpty()) {
stringResource(R.string.permissions_requested, app.permissions.size)
} else {
stringResource(R.string.details_no_permission)
},
onClick = if (app.permissions.isNotEmpty()) {
{ showExtraPane(ExtraScreen.Permission) }
} else {
null
}
)
}
item {
if (dataSafetyReport != null) {
DataSafety(
report = dataSafetyReport,
privacyPolicyUrl = app.privacyPolicyUrl
)
}
}
item {
Privacy(
report = exodusReport,
onNavigateToDetailsExodus = if (exodusReport != null &&
exodusReport.id != -1
) {
{ showExtraPane(ExtraScreen.Exodus) }
} else {
null
}
)
}
item {
DeveloperDetails(
address = app.developerAddress,
website = app.developerWebsite,
email = app.developerEmail
)
}
}
ScrollHint(
listState = listState,
bottomPadding = 5.dp,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}

View File

@@ -5,11 +5,13 @@
package com.aurora.store.compose.ui.details
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
@@ -19,6 +21,7 @@ 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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
@@ -27,6 +30,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.Constants.EXODUS_REPORT_URL
@@ -38,6 +42,7 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composable.Error
import com.aurora.store.compose.composable.Header
import com.aurora.store.compose.composable.ScrollHint
import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.composable.details.ExodusListItem
import com.aurora.store.compose.preview.AppPreviewProvider
@@ -119,33 +124,45 @@ private fun ScreenContentReport(
}
}
) { paddingValues ->
LazyColumn(
val listState = rememberLazyListState()
Box(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(horizontal = dimensionResource(R.dimen.padding_medium))
.padding(paddingValues)
) {
stickyHeader {
Surface(modifier = Modifier.fillMaxWidth()) {
Header(
title = if (report?.trackers.isNullOrEmpty()) {
stringResource(R.string.exodus_no_tracker)
} else {
stringResource(
R.string.exodus_report_trackers,
report.trackers.size,
report.version
)
},
subtitle = stringResource(R.string.exodus_view_report),
onClick = { context.browse(EXODUS_REPORT_URL + report!!.id) }
)
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
state = listState
) {
stickyHeader {
Surface(modifier = Modifier.fillMaxWidth()) {
Header(
title = if (report?.trackers.isNullOrEmpty()) {
stringResource(R.string.exodus_no_tracker)
} else {
stringResource(
R.string.exodus_report_trackers,
report.trackers.size,
report.version
)
},
subtitle = stringResource(R.string.exodus_view_report),
onClick = { context.browse(EXODUS_REPORT_URL + report!!.id) }
)
}
}
items(items = trackers, key = { item -> item.id }) { tracker ->
ExodusListItem(tracker = tracker)
}
}
items(items = trackers, key = { item -> item.id }) { tracker ->
ExodusListItem(tracker = tracker)
}
ScrollHint(
listState = listState,
bottomPadding = 5.dp,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}

View File

@@ -139,10 +139,13 @@ private fun ScreenContent(
}
}
) { paddingValues ->
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
state = listState

View File

@@ -6,14 +6,14 @@
package com.aurora.store.compose.ui.details
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.verticalScroll
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@@ -21,6 +21,7 @@ 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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLocale
import androidx.compose.ui.res.dimensionResource
@@ -30,6 +31,7 @@ import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.extensions.adaptiveNavigationIcon
@@ -38,6 +40,7 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composable.Header
import com.aurora.store.compose.composable.Info
import com.aurora.store.compose.composable.ScrollHint
import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.composable.app.AppListItem
import com.aurora.store.compose.preview.AppPreviewProvider
@@ -91,30 +94,51 @@ private fun ScreenContent(
)
}
) { paddingValues ->
Column(
val listState = rememberLazyListState()
Box(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
.padding(paddingValues)
) {
Header(title = stringResource(R.string.details_description))
Text(
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
text = AnnotatedString.fromHtml(
htmlString = app.description
),
style = MaterialTheme.typography.bodyMedium
)
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) {
item {
Header(title = stringResource(R.string.details_description))
}
if (dependencies != null) {
AppDependencies(
dependencies = dependencies,
onNavigateToAppDetails = onNavigateToAppDetails
)
item {
Text(
modifier = Modifier.padding(
horizontal = dimensionResource(R.dimen.padding_medium)
),
text = AnnotatedString.fromHtml(
htmlString = app.description
),
style = MaterialTheme.typography.bodyMedium
)
}
item {
if (dependencies != null) {
AppDependencies(
dependencies = dependencies,
onNavigateToAppDetails = onNavigateToAppDetails
)
}
}
item {
AppInfoMore(app = app)
}
}
AppInfoMore(app = app)
ScrollHint(
listState = listState,
bottomPadding = 5.dp,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}

View File

@@ -7,15 +7,18 @@ package com.aurora.store.compose.ui.details
import android.content.pm.PermissionInfo
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.foundation.lazy.rememberLazyListState
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLocale
@@ -25,6 +28,7 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.extensions.adaptiveNavigationIcon
@@ -32,6 +36,7 @@ import com.aurora.extensions.isWindowCompact
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composable.Info
import com.aurora.store.compose.composable.ScrollHint
import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.ThemePreviewProvider
@@ -84,34 +89,46 @@ private fun ScreenContent(
)
}
) { paddingValues ->
LazyColumn(
val listState = rememberLazyListState()
Box(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
.padding(paddingValues)
) {
items(items = permissionsInfo.keys.toList(), key = { it }) { permission ->
val permissionInfo = permissionsInfo.getValue(permission)
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
state = listState,
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) {
items(items = permissionsInfo.keys.toList(), key = { it }) { permission ->
val permissionInfo = permissionsInfo.getValue(permission)
Info(
title = AnnotatedString(
text = permissionInfo.loadLabel(packageManager)
.toString()
.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(LocalLocale.current.platformLocale)
} else {
it.toString()
Info(
title = AnnotatedString(
text = permissionInfo.loadLabel(packageManager)
.toString()
.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(LocalLocale.current.platformLocale)
} else {
it.toString()
}
}
}
),
description = AnnotatedString(
text = permissionInfo.loadDescription(packageManager)?.toString()
?: stringResource(R.string.no_description)
),
description = AnnotatedString(
text = permissionInfo.loadDescription(packageManager)?.toString()
?: stringResource(R.string.no_description)
)
)
)
}
}
ScrollHint(
listState = listState,
bottomPadding = 5.dp,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}

View File

@@ -6,13 +6,16 @@
package com.aurora.store.compose.ui.details
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
@@ -24,6 +27,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.res.dimensionResource
import androidx.compose.ui.res.painterResource
@@ -31,6 +35,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.LoadState
@@ -45,6 +50,7 @@ import com.aurora.gplayapi.data.models.Review
import com.aurora.store.R
import com.aurora.store.compose.composable.ContainedLoadingIndicator
import com.aurora.store.compose.composable.Error
import com.aurora.store.compose.composable.ScrollHint
import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.composable.details.ReviewListItem
import com.aurora.store.compose.preview.ReviewPreviewProvider
@@ -93,21 +99,21 @@ private fun ScreenContent(
) {
Scaffold(
topBar = {
TopAppBar(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
Column {
TopAppBar(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
FilterHeader { filter -> onFilter(filter) }
}
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(horizontal = dimensionResource(R.dimen.padding_medium))
) {
FilterHeader { filter -> onFilter(filter) }
when (reviews.loadState.refresh) {
is LoadState.Loading -> ContainedLoadingIndicator()
@@ -120,13 +126,24 @@ private fun ScreenContent(
}
else -> {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(
count = reviews.itemCount,
key = reviews.itemKey { it.commentId }
) { index ->
reviews[index]?.let { review -> ReviewListItem(review = review) }
val listState = rememberLazyListState()
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState
) {
items(
count = reviews.itemCount,
key = reviews.itemKey { it.commentId }
) { index ->
reviews[index]?.let { review -> ReviewListItem(review = review) }
}
}
ScrollHint(
listState = listState,
bottomPadding = 5.dp,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}
@@ -155,6 +172,7 @@ private fun FilterHeader(onClick: (filter: Review.Filter) -> Unit) {
LazyRow(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.margin_normal)),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_normal))
) {
items(items = filters.keys.toList(), key = { item -> item }) { filter ->

View File

@@ -7,8 +7,10 @@ package com.aurora.store.compose.ui.details.composable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
@@ -48,7 +50,9 @@ fun Actions(
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
Row(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.padding(PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium))),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
) {
val buttonWidthModifier = when {

View File

@@ -41,18 +41,27 @@ fun Changelog(changelog: String) {
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
)
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small)))
.background(color = MaterialTheme.colorScheme.secondaryContainer)
.padding(
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_small)
)
) {
Text(
text = if (changelog.isBlank()) {
AnnotatedString(text = stringResource(R.string.details_changelog_unavailable))
} else {
AnnotatedString.fromHtml(htmlString = changelog)
},
style = MaterialTheme.typography.bodyMedium
)
}
}
}

View File

@@ -105,7 +105,11 @@ fun Details(
)
}
Row(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.margin_medium))
) {
AnimatedAppIcon(
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
iconUrl = app.iconArtwork.url,

View File

@@ -87,9 +87,11 @@ fun RatingAndReviews(
)
Row(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_medium)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_large))
) {
Column(
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium)),
@@ -111,7 +113,6 @@ fun RatingAndReviews(
}
Column(
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
) {
stars.reversed().fastForEach { star ->
@@ -127,7 +128,7 @@ fun RatingAndReviews(
val pagerState = rememberPagerState { featuredReviews.size }
HorizontalPager(
state = pagerState,
contentPadding = PaddingValues(dimensionResource(R.dimen.padding_large)),
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium)),
pageSpacing = dimensionResource(R.dimen.margin_medium)
) { page ->
Box(

View File

@@ -8,6 +8,7 @@ package com.aurora.store.compose.ui.details.composable
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
@@ -34,7 +35,10 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
*/
@Composable
fun Screenshots(screenshots: List<Artwork>, onNavigateToScreenshot: (index: Int) -> Unit = {}) {
LazyRow(horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))) {
LazyRow(
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium)),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
) {
items(items = screenshots, key = { artwork -> artwork.url }) { artwork ->
ScreenshotListItem(
modifier = Modifier

View File

@@ -8,6 +8,7 @@ package com.aurora.store.compose.ui.details.composable
import android.text.format.Formatter
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
@@ -61,6 +62,7 @@ fun Tags(app: App) {
).filterKeys { it != null }
LazyRow(
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium)),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
) {
items(items = tags.keys.toList()) { label ->

View File

@@ -9,6 +9,7 @@ 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.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -40,7 +41,9 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
fun Testing(isSubscribed: Boolean, onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {}) {
Header(title = stringResource(R.string.details_beta))
Row(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_medium)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
) {

View File

@@ -17,14 +17,17 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.PreviewWrapper
import com.aurora.Constants
import com.aurora.store.R
import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.data.model.AppState
import com.aurora.store.util.PackageUtil
/**
* Menu for the app details screen
@@ -40,6 +43,7 @@ fun AppDetailsMenu(
isExpanded: Boolean = false,
onMenuItemClicked: (menuItem: MenuItem) -> Unit = {}
) {
val context = LocalContext.current
var expanded by remember { mutableStateOf(isExpanded) }
fun onClick(menuItem: MenuItem) {
onMenuItemClicked(menuItem)
@@ -87,6 +91,11 @@ fun AppDetailsMenu(
onClick = { onClick(MenuItem.ADD_TO_HOME) },
enabled = state is AppState.Installed || state is AppState.Updatable
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_view_on_play)) },
onClick = { onClick(MenuItem.PLAY_STORE) },
enabled = PackageUtil.isInstalled(context, Constants.PACKAGE_NAME_PLAY_STORE)
)
}
}
}

View File

@@ -13,5 +13,6 @@ enum class MenuItem {
SHARE,
MANUAL_DOWNLOAD,
APP_INFO,
ADD_TO_HOME
ADD_TO_HOME,
PLAY_STORE
}

View File

@@ -7,16 +7,19 @@ package com.aurora.store.compose.ui.downloads
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
@@ -25,6 +28,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.paging.LoadState
import androidx.paging.PagingData
@@ -38,6 +42,7 @@ import com.aurora.store.R
import com.aurora.store.compose.composable.ContainedLoadingIndicator
import com.aurora.store.compose.composable.DownloadListItem
import com.aurora.store.compose.composable.Error
import com.aurora.store.compose.composable.ScrollHint
import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.ThemePreviewProvider
@@ -153,23 +158,35 @@ private fun ScreenContent(
message = stringResource(R.string.download_none)
)
} else {
LazyColumn {
items(
count = downloads.itemCount,
key = downloads.itemKey { it.packageName }
) { index ->
downloads[index]?.let { download ->
DownloadListItem(
modifier = Modifier.animateItem(),
download = download,
onClick = { onNavigateToAppDetails(download.packageName) },
onClear = { onClear(download) },
onCancel = { onCancel(download.packageName) },
onExport = { onExport(download) },
onInstall = { onInstall(download) }
)
val listState = rememberLazyListState()
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
state = listState
) {
items(
count = downloads.itemCount,
key = downloads.itemKey { it.packageName }
) { index ->
downloads[index]?.let { download ->
DownloadListItem(
modifier = Modifier.animateItem(),
download = download,
onClick = {
onNavigateToAppDetails(download.packageName)
},
onClear = { onClear(download) },
onCancel = { onCancel(download.packageName) },
onExport = { onExport(download) },
onInstall = { onInstall(download) }
)
}
}
}
ScrollHint(
listState = listState,
bottomPadding = 5.dp,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}

View File

@@ -7,16 +7,19 @@ package com.aurora.store.compose.ui.favourite
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
@@ -25,6 +28,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.paging.LoadState
import androidx.paging.PagingData
@@ -38,6 +42,7 @@ import com.aurora.store.R
import com.aurora.store.compose.composable.ContainedLoadingIndicator
import com.aurora.store.compose.composable.Error
import com.aurora.store.compose.composable.FavouriteListItem
import com.aurora.store.compose.composable.ScrollHint
import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.preview.FavouritePreviewProvider
import com.aurora.store.compose.preview.ThemePreviewProvider
@@ -117,7 +122,7 @@ private fun ScreenContent(
@Composable
fun SetupMenu() {
FavouriteMenu { menuItem ->
FavouriteMenu(items = favourites.itemCount) { menuItem ->
when (menuItem) {
MenuItem.IMPORT -> onImportFavourites()
MenuItem.EXPORT -> onExportFavourites()
@@ -130,7 +135,7 @@ private fun ScreenContent(
TopAppBar(
title = stringResource(R.string.title_favourites_manager),
onNavigateUp = onNavigateUp,
actions = { if (favourites.itemCount != 0) SetupMenu() }
actions = { SetupMenu() }
)
}
) { paddingValues ->
@@ -155,20 +160,32 @@ private fun ScreenContent(
message = stringResource(R.string.details_no_favourites)
)
} else {
LazyColumn {
items(
count = favourites.itemCount,
key = favourites.itemKey { it.packageName }
) { index ->
favourites[index]?.let { favourite ->
FavouriteListItem(
modifier = Modifier.animateItem(),
favourite = favourite,
onClick = { onNavigateToAppDetails(favourite.packageName) },
onClear = { onRemoveFavourite(favourite.packageName) }
)
val listState = rememberLazyListState()
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
state = listState
) {
items(
count = favourites.itemCount,
key = favourites.itemKey { it.packageName }
) { index ->
favourites[index]?.let { favourite ->
FavouriteListItem(
modifier = Modifier.animateItem(),
favourite = favourite,
onClick = {
onNavigateToAppDetails(favourite.packageName)
},
onClear = { onRemoveFavourite(favourite.packageName) }
)
}
}
}
ScrollHint(
listState = listState,
bottomPadding = 5.dp,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}

View File

@@ -34,6 +34,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
fun FavouriteMenu(
modifier: Modifier = Modifier,
isExpanded: Boolean = false,
items: Int = 0,
onMenuItemClicked: (menuItem: MenuItem) -> Unit = {}
) {
var expanded by remember { mutableStateOf(isExpanded) }
@@ -56,7 +57,8 @@ fun FavouriteMenu(
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_export)) },
onClick = { onClick(MenuItem.EXPORT) }
onClick = { onClick(MenuItem.EXPORT) },
enabled = items > 0
)
}
}

View File

@@ -5,22 +5,26 @@
package com.aurora.store.compose.ui.installed
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.paging.LoadState
import androidx.paging.PagingData
@@ -32,6 +36,7 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composable.ContainedLoadingIndicator
import com.aurora.store.compose.composable.Error
import com.aurora.store.compose.composable.ScrollHint
import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.composable.app.LargeAppListItem
import com.aurora.store.compose.preview.AppPreviewProvider
@@ -97,18 +102,28 @@ private fun ScreenContent(
message = stringResource(R.string.no_apps_available)
)
} else {
LazyColumn {
items(
count = apps.itemCount,
key = apps.itemKey { it.packageName }
) { index ->
apps[index]?.let { app ->
LargeAppListItem(
app = app,
onClick = { onNavigateToAppDetails(app.packageName) }
)
val listState = rememberLazyListState()
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
state = listState
) {
items(
count = apps.itemCount,
key = apps.itemKey { it.packageName }
) { index ->
apps[index]?.let { app ->
LargeAppListItem(
app = app,
onClick = { onNavigateToAppDetails(app.packageName) }
)
}
}
}
ScrollHint(
listState = listState,
bottomPadding = 5.dp,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}

View File

@@ -22,12 +22,14 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@@ -82,7 +84,11 @@ private fun ScreenContent(
modifier = Modifier.height(64.dp),
title = {
PageIndicator(totalPages = pages.size, currentPage = pagerState.currentPage)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent
)
)
},
bottomBar = {

View File

@@ -6,10 +6,8 @@
package com.aurora.store.compose.ui.onboarding
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -50,23 +48,21 @@ private fun PageContent(
permissions = permissions,
onPermissionCallback = onPermissionCallback,
header = {
Surface(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
) {
Text(
text = stringResource(R.string.onboarding_title_permissions),
style = MaterialTheme.typography.headlineLargeEmphasized,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = stringResource(R.string.onboarding_permission_select),
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
Column(
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
) {
Text(
text = stringResource(R.string.onboarding_title_permissions),
style = MaterialTheme.typography.headlineLargeEmphasized,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = stringResource(R.string.onboarding_permission_select),
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
)

View File

@@ -9,12 +9,10 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -101,23 +99,21 @@ private fun PageContent(onAboutAurora: () -> Unit = {}) {
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
) {
stickyHeader {
Surface(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
) {
Text(
text = stringResource(R.string.onboarding_title_welcome),
style = MaterialTheme.typography.headlineLargeEmphasized,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = stringResource(R.string.onboarding_welcome_select),
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Column(
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
) {
Text(
text = stringResource(R.string.onboarding_title_welcome),
style = MaterialTheme.typography.headlineLargeEmphasized,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = stringResource(R.string.onboarding_welcome_select),
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}

View File

@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
@@ -45,9 +46,11 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.dimensionResource
@@ -58,6 +61,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.LoadState
@@ -70,6 +74,7 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composable.ContainedLoadingIndicator
import com.aurora.store.compose.composable.Error
import com.aurora.store.compose.composable.ScrollHint
import com.aurora.store.compose.composable.SearchSuggestionListItem
import com.aurora.store.compose.composable.app.LargeAppListItem
import com.aurora.store.compose.preview.AppPreviewProvider
@@ -205,7 +210,13 @@ private fun ScreenContent(
)
}
AppBarWithSearch(state = searchBarState, inputField = inputField)
AppBarWithSearch(
state = searchBarState,
inputField = inputField,
colors = SearchBarDefaults.appBarWithSearchColors(
appBarContainerColor = Color.Transparent
)
)
ExpandedDockedSearchBar(state = searchBarState, inputField = inputField) {
suggestions.forEach { suggestion ->
SearchSuggestionListItem(
@@ -221,7 +232,8 @@ private fun ScreenContent(
fun ListPane() {
// TODO: https://issuetracker.google.com/issues/445720462
Scaffold(
modifier = Modifier.focusable(),
modifier = Modifier
.focusable(),
topBar = { SearchBar() }
) { paddingValues ->
Column(
@@ -255,18 +267,29 @@ private fun ScreenContent(
message = stringResource(R.string.no_apps_available)
)
} else {
LazyColumn {
items(
count = results.itemCount,
key = { Uuid.random().toString() }
) { index ->
results[index]?.let { app ->
LargeAppListItem(
app = app,
onClick = { showDetailPane(app.packageName) }
)
val listState = rememberLazyListState()
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize()
) {
items(
count = results.itemCount,
key = { Uuid.random().toString() }
) { index ->
results[index]?.let { app ->
LargeAppListItem(
app = app,
onClick = { showDetailPane(app.packageName) }
)
}
}
}
ScrollHint(
listState = listState,
bottomPadding = 5.dp,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}

View File

@@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2025 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.data
data class PageResult<T>(
val items: List<T>,
val hasMore: Boolean = items.isNotEmpty()
)

View File

@@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.data.model
import android.graphics.Bitmap
data class BlacklistAppItem(
val packageName: String,
val displayName: String,
val versionName: String,
val versionCode: Long,
val icon: Bitmap,
val isFiltered: Boolean
)

View File

@@ -8,7 +8,9 @@ package com.aurora.store.data.paging
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
import androidx.paging.PagingState
import com.aurora.store.data.PageResult
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
import com.aurora.store.data.paging.GenericPagingSource.Companion.pager
import kotlinx.coroutines.Dispatchers
@@ -22,7 +24,7 @@ import kotlinx.coroutines.withContext
* @param block Data to load into the pager
*/
class GenericPagingSource<T : Any>(
private val block: suspend (Int) -> List<T>
private val block: suspend (Int) -> PageResult<T>
) : PagingSource<Int, T>() {
companion object {
@@ -53,7 +55,7 @@ class GenericPagingSource<T : Any>(
fun <T : Any> manualPager(
pageSize: Int = DEFAULT_PAGE_SIZE,
enablePlaceholders: Boolean = true,
data: suspend (Int) -> List<T>
data: suspend (Int) -> PageResult<T>
): Pager<Int, T> = Pager(
config = PagingConfig(enablePlaceholders = enablePlaceholders, pageSize = pageSize),
pagingSourceFactory = { GenericPagingSource(data) }
@@ -64,13 +66,12 @@ class GenericPagingSource<T : Any>(
val page = params.key ?: 1
return try {
withContext(Dispatchers.IO) {
val data = block(page)
val totalPages = if (data.isNotEmpty()) page + 1 else page
val result = block(page)
LoadResult.Page(
data = data,
data = result.items,
prevKey = if (page == 1) null else page - 1,
nextKey = if (page == totalPages) null else totalPages,
itemsAfter = if (page == totalPages) 0 else DEFAULT_PAGE_SIZE
nextKey = if (result.hasMore) page + 1 else null,
itemsAfter = if (result.hasMore) COUNT_UNDEFINED else 0
)
}
} catch (exception: Exception) {

View File

@@ -20,20 +20,22 @@
package com.aurora.store.viewmodel.all
import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.aurora.extensions.TAG
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.web.WebAppDetailsHelper
import com.aurora.store.data.PageResult
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.util.PackageUtil
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -47,33 +49,44 @@ class InstalledViewModel @Inject constructor(
private val webAppDetailsHelper: WebAppDetailsHelper
) : ViewModel() {
private val packages = PackageUtil.getAllValidPackages(context)
private val blacklist = blacklistProvider.blacklist
private val _apps = MutableStateFlow<PagingData<App>>(PagingData.empty())
val apps = _apps.asStateFlow()
/**
* Enumerating installed packages calls into PackageManager and resolves a label
* per app for sorting. Done lazily on [Dispatchers.IO] so the VM constructor and
* the main thread stay snappy; paging awaits this before fetching the first page.
*/
private val pagedPackages = viewModelScope.async(
context = Dispatchers.IO,
start = CoroutineStart.LAZY
) {
PackageUtil.getAllValidPackages(context)
.filterNot { it.packageName in blacklist }
.chunked(PAGE_SIZE)
}
init {
fetchApps()
}
fun fetchApps() {
val pagedPackages = packages
.filterNot { it.packageName in blacklist }
.chunked(20)
manualPager { page ->
try {
webAppDetailsHelper.getAppDetails(
pagedPackages[page].map { it.packageName }
)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch apps", exception)
emptyList()
}
private fun fetchApps() {
manualPager(pageSize = PAGE_SIZE) { page ->
// page is 1-indexed, but list is 0-indexed
val chunks = pagedPackages.await()
val chunk = chunks.getOrNull(page - 1)
?: return@manualPager PageResult(emptyList<App>(), hasMore = false)
val items = webAppDetailsHelper.getAppDetails(chunk.map { it.packageName })
PageResult(items, hasMore = page < chunks.size)
}.flow.distinctUntilChanged()
.cachedIn(viewModelScope)
.onEach { _apps.value = it }
.launchIn(viewModelScope)
}
companion object {
private const val PAGE_SIZE = 20
}
}

View File

@@ -10,12 +10,14 @@ import android.content.pm.PackageInfo
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.core.content.pm.PackageInfoCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.extensions.TAG
import com.aurora.store.AuroraApp
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.helper.UpdateHelper
import com.aurora.store.data.model.BlacklistAppItem
import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.util.CertUtil
import com.aurora.store.util.PackageUtil
@@ -44,8 +46,8 @@ class BlacklistViewModel @Inject constructor(
private val isExtendedUpdateEnabled =
Preferences.getBoolean(context, Preferences.PREFERENCE_UPDATES_EXTENDED)
private val packages = MutableStateFlow<List<PackageInfo>?>(null)
private val _filteredPackages = MutableStateFlow<List<PackageInfo>?>(null)
private val packages = MutableStateFlow<List<BlacklistAppItem>?>(null)
private val _filteredPackages = MutableStateFlow<List<BlacklistAppItem>?>(null)
val filteredPackages = _filteredPackages.asStateFlow()
val blacklist = mutableStateListOf<String>()
@@ -58,28 +60,42 @@ class BlacklistViewModel @Inject constructor(
private fun fetchApps() {
viewModelScope.launch(Dispatchers.IO) {
try {
packages.value = PackageUtil.getAllValidPackages(context).also { pkgList ->
_filteredPackages.value = pkgList
}
packages.value = PackageUtil.getAllValidPackages(context)
.mapNotNull { it.toBlacklistAppItem() }
.also { _filteredPackages.value = it }
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch apps", exception)
}
}
}
private fun PackageInfo.toBlacklistAppItem(): BlacklistAppItem? {
val icon = PackageUtil.getIconForPackage(context, packageName) ?: return null
return BlacklistAppItem(
packageName = packageName,
displayName = applicationInfo!!.loadLabel(context.packageManager).toString(),
versionName = versionName ?: "",
versionCode = PackageInfoCompat.getLongVersionCode(this),
icon = icon,
isFiltered = isFiltered(this)
)
}
fun search(query: String) {
if (query.isNotBlank()) {
_filteredPackages.value = packages.value!!
.filter {
it.applicationInfo!!.loadLabel(context.packageManager).contains(query, true) ||
viewModelScope.launch(Dispatchers.Default) {
val allPackages = packages.value ?: return@launch
if (query.isNotBlank()) {
_filteredPackages.value = allPackages.filter {
it.displayName.contains(query, true) ||
it.packageName.contains(query, true)
}
} else {
_filteredPackages.value = packages.value
} else {
_filteredPackages.value = allPackages
}
}
}
fun isFiltered(packageInfo: PackageInfo): Boolean = when {
private fun isFiltered(packageInfo: PackageInfo): Boolean = when {
!isExtendedUpdateEnabled && !packageInfo.applicationInfo!!.enabled -> true
isAuroraOnlyFilterEnabled -> !CertUtil.isAuroraStoreApp(
@@ -99,7 +115,8 @@ class BlacklistViewModel @Inject constructor(
}
fun blacklistAll() {
blacklistProvider.blacklist = packages.value!!.map { it.packageName }.toMutableSet()
val allPackages = packages.value ?: return
blacklistProvider.blacklist = allPackages.map { it.packageName }.toMutableSet()
blacklist.apply {
clear()
addAll(blacklistProvider.blacklist)
@@ -126,7 +143,10 @@ class BlacklistViewModel @Inject constructor(
)
val validImportedSet = importedSet
.filter { pkgName -> packages.value!!.any { it.packageName == pkgName } }
.filter { pkgName ->
packages.value?.any { it.packageName == pkgName } ==
true
}
blacklistProvider.blacklist.addAll(validImportedSet)
blacklist.apply {
clear()

View File

@@ -28,6 +28,7 @@ import com.aurora.extensions.TAG
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.web.WebStreamHelper
import com.aurora.store.data.PageResult
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -61,7 +62,7 @@ class StreamBrowseViewModel @AssistedInject constructor(
var nextPageUrl: String = streamCluster.clusterNextPageUrl
manualPager { page ->
try {
val items = try {
when (page) {
1 -> streamCluster.clusterAppList
@@ -79,6 +80,7 @@ class StreamBrowseViewModel @AssistedInject constructor(
Log.e(TAG, "Failed to fetch apps for $page: $nextPageUrl", exception)
emptyList()
}
PageResult(items)
}.flow.distinctUntilChanged()
.cachedIn(viewModelScope)
.onEach { _apps.value = it }

View File

@@ -13,6 +13,7 @@ import androidx.paging.cachedIn
import com.aurora.extensions.TAG
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.store.data.PageResult
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -46,7 +47,7 @@ class ReviewViewModel @AssistedInject constructor(
var reviewsNextPageUrl: String? = null
manualPager { page ->
try {
val items = try {
when (page) {
1 -> reviewsHelper.getReviews(packageName, filter).also {
reviewsNextPageUrl = it.nextPageUrl
@@ -66,6 +67,7 @@ class ReviewViewModel @AssistedInject constructor(
Log.e(TAG, "Failed to fetch reviews for $page: $reviewsNextPageUrl", exception)
emptyList()
}
PageResult(items)
}.flow.distinctUntilChanged()
.cachedIn(viewModelScope)
.onEach { _reviews.value = it }

View File

@@ -20,6 +20,7 @@ import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.SearchHelper
import com.aurora.gplayapi.helpers.contracts.SearchContract
import com.aurora.gplayapi.helpers.web.WebSearchHelper
import com.aurora.store.data.PageResult
import com.aurora.store.data.model.SearchFilter
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
import com.aurora.store.data.providers.AuthProvider
@@ -80,7 +81,7 @@ class SearchViewModel @Inject constructor(
}.distinctBy { app -> app.packageName }
manualPager { page ->
try {
val items = try {
when (page) {
1 -> contract.searchResults(query)
.also { nextBundleUrl = it.streamNextPageUrl }
@@ -110,6 +111,7 @@ class SearchViewModel @Inject constructor(
Log.e(TAG, "Failed to search results for $query", exception)
emptyList()
}
PageResult(items)
}.flow.distinctUntilChanged()
.cachedIn(viewModelScope)
.onEach { _apps.value = it }

View File

@@ -102,6 +102,7 @@
<string name="action_info">"App info"</string>
<string name="action_update">"Update"</string>
<string name="action_update_all">"Update all"</string>
<string name="action_view_on_play">"View on Play Store"</string>
<string name="action_whitelist">"Whitelist"</string>
<string name="details_beta">"Beta program"</string>
<string name="details_beta_subscribed">"You're a beta tester"</string>
@@ -492,6 +493,10 @@
<string name="default_spoof">Default</string>
<string name="available_spoof">Available</string>
<!-- BlacklistFragment -->
<string name="header_blacklist_selected">Blacklisted</string>
<string name="header_blacklist_others">Others</string>
<!-- AppDetailsFragment -->
<string name="no_description">No description available</string>
<string name="permissions_requested"><xliff:g id="number_of_permissions">%1$d</xliff:g> permissions</string>