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

View File

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

View File

@@ -5,6 +5,9 @@
package com.aurora.store.compose.theme package com.aurora.store.compose.theme
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MaterialExpressiveTheme
@@ -14,8 +17,11 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.core.view.WindowCompat
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
@@ -46,5 +52,41 @@ fun AuroraTheme(content: @Composable () -> Unit) {
else -> if (isSystemInDarkTheme()) darkScheme else lightScheme 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) 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 package com.aurora.store.compose.ui.blacklist
import android.content.pm.PackageInfo
import android.net.Uri import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts 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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.clearText
import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.material3.AppBarWithSearch import androidx.compose.material3.AppBarWithSearch
import androidx.compose.material3.ExpandedFullScreenSearchBar
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberSearchBarState import androidx.compose.material3.rememberSearchBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -30,9 +38,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.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.painterResource
@@ -40,19 +50,23 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper 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.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.Constants import com.aurora.Constants
import com.aurora.extensions.toast import com.aurora.extensions.toast
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composable.BlackListItem 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.preview.ThemePreviewProvider
import com.aurora.store.compose.ui.blacklist.menu.BlacklistMenu import com.aurora.store.compose.ui.blacklist.menu.BlacklistMenu
import com.aurora.store.compose.ui.blacklist.menu.MenuItem 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 com.aurora.store.viewmodel.blacklist.BlacklistViewModel
import java.util.Calendar import java.util.Calendar
import kotlinx.coroutines.android.awaitFrame
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -65,7 +79,6 @@ fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hi
packages = packages, packages = packages,
onNavigateUp = onNavigateUp, onNavigateUp = onNavigateUp,
isPackageBlacklisted = { pkgName -> pkgName in viewModel.blacklist }, isPackageBlacklisted = { pkgName -> pkgName in viewModel.blacklist },
isPackageFiltered = { pkgInfo -> viewModel.isFiltered(pkgInfo) },
onBlacklistImport = { uri -> onBlacklistImport = { uri ->
viewModel.importBlacklist(uri) viewModel.importBlacklist(uri)
context.toast(R.string.toast_black_import_success) context.toast(R.string.toast_black_import_success)
@@ -84,10 +97,9 @@ fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hi
@Composable @Composable
private fun ScreenContent( private fun ScreenContent(
packages: List<PackageInfo>? = null, packages: List<BlacklistAppItem>? = null,
onNavigateUp: () -> Unit = {}, onNavigateUp: () -> Unit = {},
isPackageBlacklisted: (packageName: String) -> Boolean = { false }, isPackageBlacklisted: (packageName: String) -> Boolean = { false },
isPackageFiltered: (packageInfo: PackageInfo) -> Boolean = { false },
onBlacklistImport: (uri: Uri) -> Unit = {}, onBlacklistImport: (uri: Uri) -> Unit = {},
onBlacklistExport: (uri: Uri) -> Unit = {}, onBlacklistExport: (uri: Uri) -> Unit = {},
onBlacklist: (packageName: String) -> Unit = {}, onBlacklist: (packageName: String) -> Unit = {},
@@ -112,6 +124,7 @@ private fun ScreenContent(
} }
} }
) )
val docExportLauncher = rememberLauncherForActivityResult( val docExportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE), contract = ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE),
onResult = { onResult = {
@@ -157,11 +170,23 @@ private fun ScreenContent(
@Composable @Composable
fun SearchBar() { fun SearchBar() {
val interactionSource = remember { MutableInteractionSource() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collectLatest { interaction ->
if (interaction is PressInteraction.Press) {
awaitFrame()
focusRequester.requestFocus()
}
}
}
val inputField = @Composable { val inputField = @Composable {
SearchBarDefaults.InputField( SearchBarDefaults.InputField(
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequester),
searchBarState = searchBarState, searchBarState = searchBarState,
textFieldState = textFieldState, textFieldState = textFieldState,
interactionSource = interactionSource,
onSearch = { query -> onRequestSearch(query) }, onSearch = { query -> onRequestSearch(query) },
placeholder = { placeholder = {
Text( 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 -> Scaffold(
LazyColumn( topBar = { SearchBar() }
) { paddingValues ->
val listState = rememberLazyListState()
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxSize()
.padding(paddingValues), .padding(paddingValues)
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
) { ) {
items(items = packages ?: emptyList(), key = { p -> p.packageName.hashCode() }) { pkg -> if (packages == null) {
val isBlacklisted = isPackageBlacklisted(pkg.packageName) ContainedLoadingIndicator()
val isFiltered = isPackageFiltered(pkg) } else {
BlackListItem( val (selectedPackages, otherPackages) = packages.partition { pkg ->
icon = PackageUtil.getIconForPackage(context, pkg.packageName)!!, isPackageBlacklisted(pkg.packageName) || pkg.isFiltered
displayName = pkg.applicationInfo!!.loadLabel( }
context.packageManager
).toString(), @Composable
packageName = pkg.packageName, fun BlacklistRow(pkg: BlacklistAppItem) {
versionName = pkg.versionName!!, val isBlacklisted = isPackageBlacklisted(pkg.packageName)
versionCode = PackageInfoCompat.getLongVersionCode(pkg), BlackListItem(
isChecked = isBlacklisted || isFiltered, icon = pkg.icon,
isEnabled = !isFiltered, displayName = pkg.displayName,
onClick = { packageName = pkg.packageName,
if (isBlacklisted) { versionName = pkg.versionName,
onWhitelist(pkg.packageName) versionCode = pkg.versionCode,
} else { isChecked = isBlacklisted || pkg.isFiltered,
onBlacklist(pkg.packageName) 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.fadeOut
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
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.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.WindowAdaptiveInfo 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.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.tooling.preview.PreviewWrapper 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.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
import com.aurora.Constants
import com.aurora.extensions.appInfo import com.aurora.extensions.appInfo
import com.aurora.extensions.requiresGMS import com.aurora.extensions.requiresGMS
import com.aurora.extensions.requiresObbDir 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.ContainedLoadingIndicator
import com.aurora.store.compose.composable.Error import com.aurora.store.compose.composable.Error
import com.aurora.store.compose.composable.Header 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.TopAppBar
import com.aurora.store.compose.composable.app.LargeAppListItem import com.aurora.store.compose.composable.app.LargeAppListItem
import com.aurora.store.compose.navigation.Screen import com.aurora.store.compose.navigation.Screen
@@ -163,7 +167,7 @@ fun AppDetailsScreen(
PackageUtil.getLaunchIntent(context, packageName) PackageUtil.getLaunchIntent(context, packageName)
) )
} catch (_: ActivityNotFoundException) { } catch (_: ActivityNotFoundException) {
context.toast(context.getString(R.string.unable_to_open)) context.toast(R.string.unable_to_open)
} }
}, },
onTestingSubscriptionChange = { subscribe -> onTestingSubscriptionChange = { subscribe ->
@@ -313,6 +317,21 @@ private fun ScreenContentApp(
MenuItem.ADD_TO_HOME -> { MenuItem.ADD_TO_HOME -> {
ShortcutManagerUtil.requestPinShortcut(context, app.packageName) 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 -> ) { paddingValues ->
Column( val listState = rememberLazyListState()
Box(
modifier = Modifier modifier = Modifier
.padding(paddingValues)
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .padding(paddingValues)
.padding(dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) { ) {
Details( LazyColumn(
app = app, modifier = Modifier
state = state, .fillMaxSize(),
onNavigateToDetailsDevProfile = { showExtraPane(Screen.DevProfile(it)) } verticalArrangement = Arrangement.spacedBy(
) dimensionResource(R.dimen.margin_medium)
),
SetupActions() state = listState
) {
Tags(app = app) item {
Changelog(changelog = app.changes) Details(
Header( app = app,
title = stringResource(R.string.details_more_about_app), state = state,
subtitle = app.shortDescription, onNavigateToDetailsDevProfile = { showExtraPane(Screen.DevProfile(it)) }
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
} }
)
if (dataSafetyReport != null) { item {
DataSafety(report = dataSafetyReport, privacyPolicyUrl = app.privacyPolicyUrl) SetupActions()
}
Privacy(
report = exodusReport,
onNavigateToDetailsExodus = if (exodusReport != null && exodusReport.id != -1) {
{ showExtraPane(ExtraScreen.Exodus) }
} else {
null
} }
)
DeveloperDetails( item {
address = app.developerAddress, Tags(app = app)
website = app.developerWebsite, }
email = app.developerEmail
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 package com.aurora.store.compose.ui.details
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -19,6 +21,7 @@ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource 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.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.Constants.EXODUS_REPORT_URL 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.R
import com.aurora.store.compose.composable.Error import com.aurora.store.compose.composable.Error
import com.aurora.store.compose.composable.Header 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.TopAppBar
import com.aurora.store.compose.composable.details.ExodusListItem import com.aurora.store.compose.composable.details.ExodusListItem
import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.AppPreviewProvider
@@ -119,33 +124,45 @@ private fun ScreenContentReport(
} }
} }
) { paddingValues -> ) { paddingValues ->
LazyColumn( val listState = rememberLazyListState()
Box(
modifier = Modifier modifier = Modifier
.padding(paddingValues)
.fillMaxSize() .fillMaxSize()
.padding(horizontal = dimensionResource(R.dimen.padding_medium)) .padding(paddingValues)
) { ) {
stickyHeader { LazyColumn(
Surface(modifier = Modifier.fillMaxWidth()) { modifier = Modifier
Header( .fillMaxSize()
title = if (report?.trackers.isNullOrEmpty()) { .padding(horizontal = dimensionResource(R.dimen.padding_medium)),
stringResource(R.string.exodus_no_tracker) state = listState
} else { ) {
stringResource( stickyHeader {
R.string.exodus_report_trackers, Surface(modifier = Modifier.fillMaxWidth()) {
report.trackers.size, Header(
report.version title = if (report?.trackers.isNullOrEmpty()) {
) stringResource(R.string.exodus_no_tracker)
}, } else {
subtitle = stringResource(R.string.exodus_view_report), stringResource(
onClick = { context.browse(EXODUS_REPORT_URL + report!!.id) } 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)
} }
} }
ScrollHint(
items(items = trackers, key = { item -> item.id }) { tracker -> listState = listState,
ExodusListItem(tracker = tracker) bottomPadding = 5.dp,
} modifier = Modifier.align(Alignment.BottomCenter)
)
} }
} }
} }

View File

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

View File

@@ -6,14 +6,14 @@
package com.aurora.store.compose.ui.details package com.aurora.store.compose.ui.details
import androidx.compose.foundation.layout.Arrangement 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -21,6 +21,7 @@ import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLocale import androidx.compose.ui.platform.LocalLocale
import androidx.compose.ui.res.dimensionResource 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.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.extensions.adaptiveNavigationIcon 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.R
import com.aurora.store.compose.composable.Header import com.aurora.store.compose.composable.Header
import com.aurora.store.compose.composable.Info 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.TopAppBar
import com.aurora.store.compose.composable.app.AppListItem import com.aurora.store.compose.composable.app.AppListItem
import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.AppPreviewProvider
@@ -91,30 +94,51 @@ private fun ScreenContent(
) )
} }
) { paddingValues -> ) { paddingValues ->
Column( val listState = rememberLazyListState()
Box(
modifier = Modifier modifier = Modifier
.padding(paddingValues)
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()), .padding(paddingValues)
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) { ) {
Header(title = stringResource(R.string.details_description)) LazyColumn(
Text( modifier = Modifier.fillMaxSize(),
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.padding_medium)), state = listState,
text = AnnotatedString.fromHtml( verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
htmlString = app.description ) {
), item {
style = MaterialTheme.typography.bodyMedium Header(title = stringResource(R.string.details_description))
) }
if (dependencies != null) { item {
AppDependencies( Text(
dependencies = dependencies, modifier = Modifier.padding(
onNavigateToAppDetails = onNavigateToAppDetails 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)
}
} }
ScrollHint(
AppInfoMore(app = app) 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 android.content.pm.PermissionInfo
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLocale 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.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.extensions.adaptiveNavigationIcon import com.aurora.extensions.adaptiveNavigationIcon
@@ -32,6 +36,7 @@ import com.aurora.extensions.isWindowCompact
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composable.Info 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.TopAppBar
import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider
@@ -84,34 +89,46 @@ private fun ScreenContent(
) )
} }
) { paddingValues -> ) { paddingValues ->
LazyColumn( val listState = rememberLazyListState()
Box(
modifier = Modifier modifier = Modifier
.padding(paddingValues)
.fillMaxSize() .fillMaxSize()
.padding(horizontal = dimensionResource(R.dimen.padding_medium)), .padding(paddingValues)
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) { ) {
items(items = permissionsInfo.keys.toList(), key = { it }) { permission -> LazyColumn(
val permissionInfo = permissionsInfo.getValue(permission) 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( Info(
title = AnnotatedString( title = AnnotatedString(
text = permissionInfo.loadLabel(packageManager) text = permissionInfo.loadLabel(packageManager)
.toString() .toString()
.replaceFirstChar { .replaceFirstChar {
if (it.isLowerCase()) { if (it.isLowerCase()) {
it.titlecase(LocalLocale.current.platformLocale) it.titlecase(LocalLocale.current.platformLocale)
} else { } else {
it.toString() it.toString()
}
} }
} ),
), description = AnnotatedString(
description = AnnotatedString( text = permissionInfo.loadDescription(packageManager)?.toString()
text = permissionInfo.loadDescription(packageManager)?.toString() ?: stringResource(R.string.no_description)
?: 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 package com.aurora.store.compose.ui.details
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -24,6 +27,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
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.res.dimensionResource import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource 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.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.LoadState import androidx.paging.LoadState
@@ -45,6 +50,7 @@ import com.aurora.gplayapi.data.models.Review
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composable.ContainedLoadingIndicator import com.aurora.store.compose.composable.ContainedLoadingIndicator
import com.aurora.store.compose.composable.Error 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.TopAppBar
import com.aurora.store.compose.composable.details.ReviewListItem import com.aurora.store.compose.composable.details.ReviewListItem
import com.aurora.store.compose.preview.ReviewPreviewProvider import com.aurora.store.compose.preview.ReviewPreviewProvider
@@ -93,21 +99,21 @@ private fun ScreenContent(
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( Column {
title = topAppBarTitle, TopAppBar(
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, title = topAppBarTitle,
onNavigateUp = onNavigateUp navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
) onNavigateUp = onNavigateUp
)
FilterHeader { filter -> onFilter(filter) }
}
} }
) { paddingValues -> ) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
.padding(paddingValues) .padding(paddingValues)
.fillMaxSize() .fillMaxSize()
.padding(horizontal = dimensionResource(R.dimen.padding_medium))
) { ) {
FilterHeader { filter -> onFilter(filter) }
when (reviews.loadState.refresh) { when (reviews.loadState.refresh) {
is LoadState.Loading -> ContainedLoadingIndicator() is LoadState.Loading -> ContainedLoadingIndicator()
@@ -120,13 +126,24 @@ private fun ScreenContent(
} }
else -> { else -> {
LazyColumn(modifier = Modifier.fillMaxSize()) { val listState = rememberLazyListState()
items( Box(modifier = Modifier.fillMaxSize()) {
count = reviews.itemCount, LazyColumn(
key = reviews.itemKey { it.commentId } modifier = Modifier.fillMaxSize(),
) { index -> state = listState
reviews[index]?.let { review -> ReviewListItem(review = review) } ) {
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( LazyRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.margin_normal)),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_normal)) horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_normal))
) { ) {
items(items = filters.keys.toList(), key = { item -> item }) { filter -> 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
@@ -48,7 +50,9 @@ fun Actions(
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo() windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.padding(PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium))),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium)) horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
) { ) {
val buttonWidthModifier = when { val buttonWidthModifier = when {

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ package com.aurora.store.compose.ui.details.composable
import android.text.format.Formatter import android.text.format.Formatter
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -61,6 +62,7 @@ fun Tags(app: App) {
).filterKeys { it != null } ).filterKeys { it != null }
LazyRow( LazyRow(
contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_medium)),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium)) horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
) { ) {
items(items = tags.keys.toList()) { label -> 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -40,7 +41,9 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
fun Testing(isSubscribed: Boolean, onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {}) { fun Testing(isSubscribed: Boolean, onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {}) {
Header(title = stringResource(R.string.details_beta)) Header(title = stringResource(R.string.details_beta))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_medium)),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small)) 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.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.compose.ui.tooling.preview.PreviewWrapper
import com.aurora.Constants
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composable.TopAppBar import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.data.model.AppState import com.aurora.store.data.model.AppState
import com.aurora.store.util.PackageUtil
/** /**
* Menu for the app details screen * Menu for the app details screen
@@ -40,6 +43,7 @@ fun AppDetailsMenu(
isExpanded: Boolean = false, isExpanded: Boolean = false,
onMenuItemClicked: (menuItem: MenuItem) -> Unit = {} onMenuItemClicked: (menuItem: MenuItem) -> Unit = {}
) { ) {
val context = LocalContext.current
var expanded by remember { mutableStateOf(isExpanded) } var expanded by remember { mutableStateOf(isExpanded) }
fun onClick(menuItem: MenuItem) { fun onClick(menuItem: MenuItem) {
onMenuItemClicked(menuItem) onMenuItemClicked(menuItem)
@@ -87,6 +91,11 @@ fun AppDetailsMenu(
onClick = { onClick(MenuItem.ADD_TO_HOME) }, onClick = { onClick(MenuItem.ADD_TO_HOME) },
enabled = state is AppState.Installed || state is AppState.Updatable 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, SHARE,
MANUAL_DOWNLOAD, MANUAL_DOWNLOAD,
APP_INFO, 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.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
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.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource 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.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.PagingData 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.ContainedLoadingIndicator
import com.aurora.store.compose.composable.DownloadListItem import com.aurora.store.compose.composable.DownloadListItem
import com.aurora.store.compose.composable.Error 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.TopAppBar
import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider
@@ -153,23 +158,35 @@ private fun ScreenContent(
message = stringResource(R.string.download_none) message = stringResource(R.string.download_none)
) )
} else { } else {
LazyColumn { val listState = rememberLazyListState()
items( Box(modifier = Modifier.fillMaxSize()) {
count = downloads.itemCount, LazyColumn(
key = downloads.itemKey { it.packageName } state = listState
) { index -> ) {
downloads[index]?.let { download -> items(
DownloadListItem( count = downloads.itemCount,
modifier = Modifier.animateItem(), key = downloads.itemKey { it.packageName }
download = download, ) { index ->
onClick = { onNavigateToAppDetails(download.packageName) }, downloads[index]?.let { download ->
onClear = { onClear(download) }, DownloadListItem(
onCancel = { onCancel(download.packageName) }, modifier = Modifier.animateItem(),
onExport = { onExport(download) }, download = download,
onInstall = { onInstall(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.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
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.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource 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.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.PagingData 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.ContainedLoadingIndicator
import com.aurora.store.compose.composable.Error import com.aurora.store.compose.composable.Error
import com.aurora.store.compose.composable.FavouriteListItem 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.composable.TopAppBar
import com.aurora.store.compose.preview.FavouritePreviewProvider import com.aurora.store.compose.preview.FavouritePreviewProvider
import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider
@@ -117,7 +122,7 @@ private fun ScreenContent(
@Composable @Composable
fun SetupMenu() { fun SetupMenu() {
FavouriteMenu { menuItem -> FavouriteMenu(items = favourites.itemCount) { menuItem ->
when (menuItem) { when (menuItem) {
MenuItem.IMPORT -> onImportFavourites() MenuItem.IMPORT -> onImportFavourites()
MenuItem.EXPORT -> onExportFavourites() MenuItem.EXPORT -> onExportFavourites()
@@ -130,7 +135,7 @@ private fun ScreenContent(
TopAppBar( TopAppBar(
title = stringResource(R.string.title_favourites_manager), title = stringResource(R.string.title_favourites_manager),
onNavigateUp = onNavigateUp, onNavigateUp = onNavigateUp,
actions = { if (favourites.itemCount != 0) SetupMenu() } actions = { SetupMenu() }
) )
} }
) { paddingValues -> ) { paddingValues ->
@@ -155,20 +160,32 @@ private fun ScreenContent(
message = stringResource(R.string.details_no_favourites) message = stringResource(R.string.details_no_favourites)
) )
} else { } else {
LazyColumn { val listState = rememberLazyListState()
items( Box(modifier = Modifier.fillMaxSize()) {
count = favourites.itemCount, LazyColumn(
key = favourites.itemKey { it.packageName } state = listState
) { index -> ) {
favourites[index]?.let { favourite -> items(
FavouriteListItem( count = favourites.itemCount,
modifier = Modifier.animateItem(), key = favourites.itemKey { it.packageName }
favourite = favourite, ) { index ->
onClick = { onNavigateToAppDetails(favourite.packageName) }, favourites[index]?.let { favourite ->
onClear = { onRemoveFavourite(favourite.packageName) } 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( fun FavouriteMenu(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isExpanded: Boolean = false, isExpanded: Boolean = false,
items: Int = 0,
onMenuItemClicked: (menuItem: MenuItem) -> Unit = {} onMenuItemClicked: (menuItem: MenuItem) -> Unit = {}
) { ) {
var expanded by remember { mutableStateOf(isExpanded) } var expanded by remember { mutableStateOf(isExpanded) }
@@ -56,7 +57,8 @@ fun FavouriteMenu(
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_export)) }, 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 package com.aurora.store.compose.ui.installed
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
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.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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.PreviewWrapper import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.PagingData import androidx.paging.PagingData
@@ -32,6 +36,7 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composable.ContainedLoadingIndicator import com.aurora.store.compose.composable.ContainedLoadingIndicator
import com.aurora.store.compose.composable.Error 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.TopAppBar
import com.aurora.store.compose.composable.app.LargeAppListItem import com.aurora.store.compose.composable.app.LargeAppListItem
import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.AppPreviewProvider
@@ -97,18 +102,28 @@ private fun ScreenContent(
message = stringResource(R.string.no_apps_available) message = stringResource(R.string.no_apps_available)
) )
} else { } else {
LazyColumn { val listState = rememberLazyListState()
items( Box(modifier = Modifier.fillMaxSize()) {
count = apps.itemCount, LazyColumn(
key = apps.itemKey { it.packageName } state = listState
) { index -> ) {
apps[index]?.let { app -> items(
LargeAppListItem( count = apps.itemCount,
app = app, key = apps.itemKey { it.packageName }
onClick = { onNavigateToAppDetails(app.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.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -82,7 +84,11 @@ private fun ScreenContent(
modifier = Modifier.height(64.dp), modifier = Modifier.height(64.dp),
title = { title = {
PageIndicator(totalPages = pages.size, currentPage = pagerState.currentPage) PageIndicator(totalPages = pages.size, currentPage = pagerState.currentPage)
} },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent
)
) )
}, },
bottomBar = { bottomBar = {

View File

@@ -6,10 +6,8 @@
package com.aurora.store.compose.ui.onboarding package com.aurora.store.compose.ui.onboarding
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -50,23 +48,21 @@ private fun PageContent(
permissions = permissions, permissions = permissions,
onPermissionCallback = onPermissionCallback, onPermissionCallback = onPermissionCallback,
header = { header = {
Surface(modifier = Modifier.fillMaxWidth()) { Column(
Column( modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium)) ) {
) { Text(
Text( text = stringResource(R.string.onboarding_title_permissions),
text = stringResource(R.string.onboarding_title_permissions), style = MaterialTheme.typography.headlineLargeEmphasized,
style = MaterialTheme.typography.headlineLargeEmphasized, maxLines = 1,
maxLines = 1, overflow = TextOverflow.Ellipsis
overflow = TextOverflow.Ellipsis )
) Text(
Text( text = stringResource(R.string.onboarding_permission_select),
text = stringResource(R.string.onboarding_permission_select), style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleMedium, maxLines = 2,
maxLines = 2, overflow = TextOverflow.Ellipsis
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.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -101,23 +99,21 @@ private fun PageContent(onAboutAurora: () -> Unit = {}) {
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall)) verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
) { ) {
stickyHeader { stickyHeader {
Surface(modifier = Modifier.fillMaxWidth()) { Column(
Column( modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium)) ) {
) { Text(
Text( text = stringResource(R.string.onboarding_title_welcome),
text = stringResource(R.string.onboarding_title_welcome), style = MaterialTheme.typography.headlineLargeEmphasized,
style = MaterialTheme.typography.headlineLargeEmphasized, maxLines = 1,
maxLines = 1, overflow = TextOverflow.Ellipsis
overflow = TextOverflow.Ellipsis )
) Text(
Text( text = stringResource(R.string.onboarding_welcome_select),
text = stringResource(R.string.onboarding_welcome_select), style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleMedium, maxLines = 1,
maxLines = 1, overflow = TextOverflow.Ellipsis
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.LazyColumn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items 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.clearText
import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd 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.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.dimensionResource 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.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.LoadState import androidx.paging.LoadState
@@ -70,6 +74,7 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.composable.ContainedLoadingIndicator import com.aurora.store.compose.composable.ContainedLoadingIndicator
import com.aurora.store.compose.composable.Error 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.SearchSuggestionListItem
import com.aurora.store.compose.composable.app.LargeAppListItem import com.aurora.store.compose.composable.app.LargeAppListItem
import com.aurora.store.compose.preview.AppPreviewProvider 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) { ExpandedDockedSearchBar(state = searchBarState, inputField = inputField) {
suggestions.forEach { suggestion -> suggestions.forEach { suggestion ->
SearchSuggestionListItem( SearchSuggestionListItem(
@@ -221,7 +232,8 @@ private fun ScreenContent(
fun ListPane() { fun ListPane() {
// TODO: https://issuetracker.google.com/issues/445720462 // TODO: https://issuetracker.google.com/issues/445720462
Scaffold( Scaffold(
modifier = Modifier.focusable(), modifier = Modifier
.focusable(),
topBar = { SearchBar() } topBar = { SearchBar() }
) { paddingValues -> ) { paddingValues ->
Column( Column(
@@ -255,18 +267,29 @@ private fun ScreenContent(
message = stringResource(R.string.no_apps_available) message = stringResource(R.string.no_apps_available)
) )
} else { } else {
LazyColumn { val listState = rememberLazyListState()
items( Box(modifier = Modifier.fillMaxSize()) {
count = results.itemCount, LazyColumn(
key = { Uuid.random().toString() } state = listState,
) { index -> modifier = Modifier.fillMaxSize()
results[index]?.let { app -> ) {
LargeAppListItem( items(
app = app, count = results.itemCount,
onClick = { showDetailPane(app.packageName) } 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.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
import androidx.paging.PagingState 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.manualPager
import com.aurora.store.data.paging.GenericPagingSource.Companion.pager import com.aurora.store.data.paging.GenericPagingSource.Companion.pager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -22,7 +24,7 @@ import kotlinx.coroutines.withContext
* @param block Data to load into the pager * @param block Data to load into the pager
*/ */
class GenericPagingSource<T : Any>( class GenericPagingSource<T : Any>(
private val block: suspend (Int) -> List<T> private val block: suspend (Int) -> PageResult<T>
) : PagingSource<Int, T>() { ) : PagingSource<Int, T>() {
companion object { companion object {
@@ -53,7 +55,7 @@ class GenericPagingSource<T : Any>(
fun <T : Any> manualPager( fun <T : Any> manualPager(
pageSize: Int = DEFAULT_PAGE_SIZE, pageSize: Int = DEFAULT_PAGE_SIZE,
enablePlaceholders: Boolean = true, enablePlaceholders: Boolean = true,
data: suspend (Int) -> List<T> data: suspend (Int) -> PageResult<T>
): Pager<Int, T> = Pager( ): Pager<Int, T> = Pager(
config = PagingConfig(enablePlaceholders = enablePlaceholders, pageSize = pageSize), config = PagingConfig(enablePlaceholders = enablePlaceholders, pageSize = pageSize),
pagingSourceFactory = { GenericPagingSource(data) } pagingSourceFactory = { GenericPagingSource(data) }
@@ -64,13 +66,12 @@ class GenericPagingSource<T : Any>(
val page = params.key ?: 1 val page = params.key ?: 1
return try { return try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val data = block(page) val result = block(page)
val totalPages = if (data.isNotEmpty()) page + 1 else page
LoadResult.Page( LoadResult.Page(
data = data, data = result.items,
prevKey = if (page == 1) null else page - 1, prevKey = if (page == 1) null else page - 1,
nextKey = if (page == totalPages) null else totalPages, nextKey = if (result.hasMore) page + 1 else null,
itemsAfter = if (page == totalPages) 0 else DEFAULT_PAGE_SIZE itemsAfter = if (result.hasMore) COUNT_UNDEFINED else 0
) )
} }
} catch (exception: Exception) { } catch (exception: Exception) {

View File

@@ -20,20 +20,22 @@
package com.aurora.store.viewmodel.all package com.aurora.store.viewmodel.all
import android.content.Context import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import com.aurora.extensions.TAG
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.web.WebAppDetailsHelper 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.paging.GenericPagingSource.Companion.manualPager
import com.aurora.store.data.providers.BlacklistProvider import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject 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.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@@ -47,33 +49,44 @@ class InstalledViewModel @Inject constructor(
private val webAppDetailsHelper: WebAppDetailsHelper private val webAppDetailsHelper: WebAppDetailsHelper
) : ViewModel() { ) : ViewModel() {
private val packages = PackageUtil.getAllValidPackages(context)
private val blacklist = blacklistProvider.blacklist private val blacklist = blacklistProvider.blacklist
private val _apps = MutableStateFlow<PagingData<App>>(PagingData.empty()) private val _apps = MutableStateFlow<PagingData<App>>(PagingData.empty())
val apps = _apps.asStateFlow() 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 { init {
fetchApps() fetchApps()
} }
fun fetchApps() { private fun fetchApps() {
val pagedPackages = packages manualPager(pageSize = PAGE_SIZE) { page ->
.filterNot { it.packageName in blacklist } // page is 1-indexed, but list is 0-indexed
.chunked(20) val chunks = pagedPackages.await()
val chunk = chunks.getOrNull(page - 1)
manualPager { page -> ?: return@manualPager PageResult(emptyList<App>(), hasMore = false)
try { val items = webAppDetailsHelper.getAppDetails(chunk.map { it.packageName })
webAppDetailsHelper.getAppDetails( PageResult(items, hasMore = page < chunks.size)
pagedPackages[page].map { it.packageName }
)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch apps", exception)
emptyList()
}
}.flow.distinctUntilChanged() }.flow.distinctUntilChanged()
.cachedIn(viewModelScope) .cachedIn(viewModelScope)
.onEach { _apps.value = it } .onEach { _apps.value = it }
.launchIn(viewModelScope) .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.net.Uri
import android.util.Log import android.util.Log
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.core.content.pm.PackageInfoCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.extensions.TAG import com.aurora.extensions.TAG
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.data.event.BusEvent import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.helper.UpdateHelper 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.data.providers.BlacklistProvider
import com.aurora.store.util.CertUtil import com.aurora.store.util.CertUtil
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
@@ -44,8 +46,8 @@ class BlacklistViewModel @Inject constructor(
private val isExtendedUpdateEnabled = private val isExtendedUpdateEnabled =
Preferences.getBoolean(context, Preferences.PREFERENCE_UPDATES_EXTENDED) Preferences.getBoolean(context, Preferences.PREFERENCE_UPDATES_EXTENDED)
private val packages = MutableStateFlow<List<PackageInfo>?>(null) private val packages = MutableStateFlow<List<BlacklistAppItem>?>(null)
private val _filteredPackages = MutableStateFlow<List<PackageInfo>?>(null) private val _filteredPackages = MutableStateFlow<List<BlacklistAppItem>?>(null)
val filteredPackages = _filteredPackages.asStateFlow() val filteredPackages = _filteredPackages.asStateFlow()
val blacklist = mutableStateListOf<String>() val blacklist = mutableStateListOf<String>()
@@ -58,28 +60,42 @@ class BlacklistViewModel @Inject constructor(
private fun fetchApps() { private fun fetchApps() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
packages.value = PackageUtil.getAllValidPackages(context).also { pkgList -> packages.value = PackageUtil.getAllValidPackages(context)
_filteredPackages.value = pkgList .mapNotNull { it.toBlacklistAppItem() }
} .also { _filteredPackages.value = it }
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to fetch apps", 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) { fun search(query: String) {
if (query.isNotBlank()) { viewModelScope.launch(Dispatchers.Default) {
_filteredPackages.value = packages.value!! val allPackages = packages.value ?: return@launch
.filter { if (query.isNotBlank()) {
it.applicationInfo!!.loadLabel(context.packageManager).contains(query, true) || _filteredPackages.value = allPackages.filter {
it.displayName.contains(query, true) ||
it.packageName.contains(query, true) it.packageName.contains(query, true)
} }
} else { } else {
_filteredPackages.value = packages.value _filteredPackages.value = allPackages
}
} }
} }
fun isFiltered(packageInfo: PackageInfo): Boolean = when { private fun isFiltered(packageInfo: PackageInfo): Boolean = when {
!isExtendedUpdateEnabled && !packageInfo.applicationInfo!!.enabled -> true !isExtendedUpdateEnabled && !packageInfo.applicationInfo!!.enabled -> true
isAuroraOnlyFilterEnabled -> !CertUtil.isAuroraStoreApp( isAuroraOnlyFilterEnabled -> !CertUtil.isAuroraStoreApp(
@@ -99,7 +115,8 @@ class BlacklistViewModel @Inject constructor(
} }
fun blacklistAll() { fun blacklistAll() {
blacklistProvider.blacklist = packages.value!!.map { it.packageName }.toMutableSet() val allPackages = packages.value ?: return
blacklistProvider.blacklist = allPackages.map { it.packageName }.toMutableSet()
blacklist.apply { blacklist.apply {
clear() clear()
addAll(blacklistProvider.blacklist) addAll(blacklistProvider.blacklist)
@@ -126,7 +143,10 @@ class BlacklistViewModel @Inject constructor(
) )
val validImportedSet = importedSet val validImportedSet = importedSet
.filter { pkgName -> packages.value!!.any { it.packageName == pkgName } } .filter { pkgName ->
packages.value?.any { it.packageName == pkgName } ==
true
}
blacklistProvider.blacklist.addAll(validImportedSet) blacklistProvider.blacklist.addAll(validImportedSet)
blacklist.apply { blacklist.apply {
clear() 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.App
import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.web.WebStreamHelper import com.aurora.gplayapi.helpers.web.WebStreamHelper
import com.aurora.store.data.PageResult
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@@ -61,7 +62,7 @@ class StreamBrowseViewModel @AssistedInject constructor(
var nextPageUrl: String = streamCluster.clusterNextPageUrl var nextPageUrl: String = streamCluster.clusterNextPageUrl
manualPager { page -> manualPager { page ->
try { val items = try {
when (page) { when (page) {
1 -> streamCluster.clusterAppList 1 -> streamCluster.clusterAppList
@@ -79,6 +80,7 @@ class StreamBrowseViewModel @AssistedInject constructor(
Log.e(TAG, "Failed to fetch apps for $page: $nextPageUrl", exception) Log.e(TAG, "Failed to fetch apps for $page: $nextPageUrl", exception)
emptyList() emptyList()
} }
PageResult(items)
}.flow.distinctUntilChanged() }.flow.distinctUntilChanged()
.cachedIn(viewModelScope) .cachedIn(viewModelScope)
.onEach { _apps.value = it } .onEach { _apps.value = it }

View File

@@ -13,6 +13,7 @@ import androidx.paging.cachedIn
import com.aurora.extensions.TAG import com.aurora.extensions.TAG
import com.aurora.gplayapi.data.models.Review import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.helpers.ReviewsHelper import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.store.data.PageResult
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@@ -46,7 +47,7 @@ class ReviewViewModel @AssistedInject constructor(
var reviewsNextPageUrl: String? = null var reviewsNextPageUrl: String? = null
manualPager { page -> manualPager { page ->
try { val items = try {
when (page) { when (page) {
1 -> reviewsHelper.getReviews(packageName, filter).also { 1 -> reviewsHelper.getReviews(packageName, filter).also {
reviewsNextPageUrl = it.nextPageUrl reviewsNextPageUrl = it.nextPageUrl
@@ -66,6 +67,7 @@ class ReviewViewModel @AssistedInject constructor(
Log.e(TAG, "Failed to fetch reviews for $page: $reviewsNextPageUrl", exception) Log.e(TAG, "Failed to fetch reviews for $page: $reviewsNextPageUrl", exception)
emptyList() emptyList()
} }
PageResult(items)
}.flow.distinctUntilChanged() }.flow.distinctUntilChanged()
.cachedIn(viewModelScope) .cachedIn(viewModelScope)
.onEach { _reviews.value = it } .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.SearchHelper
import com.aurora.gplayapi.helpers.contracts.SearchContract import com.aurora.gplayapi.helpers.contracts.SearchContract
import com.aurora.gplayapi.helpers.web.WebSearchHelper 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.model.SearchFilter
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
@@ -80,7 +81,7 @@ class SearchViewModel @Inject constructor(
}.distinctBy { app -> app.packageName } }.distinctBy { app -> app.packageName }
manualPager { page -> manualPager { page ->
try { val items = try {
when (page) { when (page) {
1 -> contract.searchResults(query) 1 -> contract.searchResults(query)
.also { nextBundleUrl = it.streamNextPageUrl } .also { nextBundleUrl = it.streamNextPageUrl }
@@ -110,6 +111,7 @@ class SearchViewModel @Inject constructor(
Log.e(TAG, "Failed to search results for $query", exception) Log.e(TAG, "Failed to search results for $query", exception)
emptyList() emptyList()
} }
PageResult(items)
}.flow.distinctUntilChanged() }.flow.distinctUntilChanged()
.cachedIn(viewModelScope) .cachedIn(viewModelScope)
.onEach { _apps.value = it } .onEach { _apps.value = it }

View File

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