Merge branch 'dev' into 'master'
dev See merge request AuroraOSS/AuroraStore!558
This commit is contained in:
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
@@ -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))
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
app/src/main/java/com/aurora/store/data/PageResult.kt
Normal file
11
app/src/main/java/com/aurora/store/data/PageResult.kt
Normal 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()
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user