compose: details: Setup menu

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-05-16 17:49:38 +08:00
parent 6c38ff5479
commit e7f6d40709
5 changed files with 193 additions and 13 deletions

View File

@@ -32,6 +32,7 @@ import android.graphics.Color
import android.os.Bundle
import android.os.Environment
import android.os.PowerManager
import android.provider.Settings
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
@@ -60,6 +61,17 @@ fun Context.browse(url: String) {
}
}
fun Context.appInfo(packageName: String) {
try {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = "package:$packageName".toUri()
}
startActivity(intent)
} catch (exception: Exception) {
Log.e(TAG, "Failed to open app info", exception)
}
}
fun Context.share(app: App) {
try {
val sendIntent = Intent().apply {

View File

@@ -0,0 +1,112 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.menu
import androidx.compose.foundation.layout.Box
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.compose.composables.preview.AppPreviewProvider
import com.aurora.store.compose.menu.items.AppDetailsMenuItem
/**
* Menu for the app details screen
* @param app App for which this menu should be inflated
* @param modifier The modifier to be applied to the composable
* @param onMenuItemClicked Callback when a menu item has been clicked
* @see AppDetailsMenuItem
*/
@Composable
fun AppDetailsMenu(
modifier: Modifier = Modifier,
isInstalled: Boolean = false,
isFavorite: Boolean = false,
isExpanded: Boolean = false,
onMenuItemClicked: (appDetailsMenuItem: AppDetailsMenuItem) -> Unit = {}
) {
var expanded by remember { mutableStateOf(isExpanded) }
fun onClick(appDetailsMenuItem: AppDetailsMenuItem) {
onMenuItemClicked(appDetailsMenuItem)
expanded = false
}
IconButton(onClick = { onClick(AppDetailsMenuItem.FAVORITE) }) {
Icon(
painter = if (isFavorite) {
painterResource(R.drawable.ic_favorite_checked)
} else {
painterResource(R.drawable.ic_favorite_unchecked)
},
contentDescription = stringResource(R.string.action_favourite)
)
}
IconButton(onClick = { onClick(AppDetailsMenuItem.SHARE) }) {
Icon(
painter = painterResource(R.drawable.ic_share),
contentDescription = stringResource(R.string.action_share)
)
}
Box(modifier = modifier) {
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.menu))
}
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.title_manual_download)) },
onClick = { onClick(AppDetailsMenuItem.MANUAL_DOWNLOAD) }
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.title_download_playstore)) },
onClick = { onClick(AppDetailsMenuItem.PLAY_STORE) }
)
// Inflate actions available only when app is installed below
if (!isInstalled) return@DropdownMenu
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_info)) },
onClick = { onClick(AppDetailsMenuItem.APP_INFO) }
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_home_screen)) },
onClick = { onClick(AppDetailsMenuItem.ADD_TO_HOME) }
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun AppDetailsMenuPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
TopAppBarComposable(
actions = {
AppDetailsMenu(
isInstalled = app.isInstalled,
isFavorite = true,
isExpanded = true
)
}
)
}

View File

@@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.menu.items
/**
* Valid menu items for app details screen for handling clicks
*/
enum class AppDetailsMenuItem {
FAVORITE,
SHARE,
MANUAL_DOWNLOAD,
APP_INFO,
PLAY_STORE,
ADD_TO_HOME
}

View File

@@ -8,6 +8,7 @@ package com.aurora.store.compose.ui.details
import android.content.ActivityNotFoundException
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
@@ -15,6 +16,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.NavigableSupportingPaneScaffold
import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator
@@ -37,6 +39,10 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.LocalAsyncImagePreviewHandler
import com.aurora.Constants.SHARE_URL
import com.aurora.extensions.appInfo
import com.aurora.extensions.browse
import com.aurora.extensions.share
import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
@@ -46,6 +52,8 @@ import com.aurora.store.compose.composables.app.AppProgressComposable
import com.aurora.store.compose.composables.app.NoAppComposable
import com.aurora.store.compose.composables.preview.AppPreviewProvider
import com.aurora.store.compose.composables.preview.coilPreviewProvider
import com.aurora.store.compose.menu.AppDetailsMenu
import com.aurora.store.compose.menu.items.AppDetailsMenuItem
import com.aurora.store.compose.navigation.Screen
import com.aurora.store.compose.ui.details.components.AppActions
import com.aurora.store.compose.ui.details.components.AppChangelog
@@ -66,6 +74,7 @@ import com.aurora.store.data.model.Scores
import com.aurora.store.data.room.download.Download
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.PackageUtil.PACKAGE_NAME_GMS
import com.aurora.store.util.ShortcutManagerUtil
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import kotlinx.coroutines.launch
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
@@ -80,6 +89,7 @@ fun AppDetailsScreen(
val context = LocalContext.current
val app by viewModel.app.collectAsStateWithLifecycle()
val favorite by viewModel.favourite.collectAsStateWithLifecycle()
val exodusReport by viewModel.exodusReport.collectAsStateWithLifecycle()
val dataSafetyReport by viewModel.dataSafetyReport.collectAsStateWithLifecycle()
val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle()
@@ -122,6 +132,7 @@ fun AppDetailsScreen(
} else {
ScreenContentApp(
app = this,
isFavorite = favorite,
isAnonymous = viewModel.authProvider.isAnonymous,
download = download,
installProgress = installProgress,
@@ -132,6 +143,7 @@ fun AppDetailsScreen(
onNavigateUp = onNavigateUp,
onNavigateToAppDetails = onNavigateToAppDetails,
onDownload = { viewModel.download(this) },
onFavorite = { viewModel.toggleFavourite(this) },
onManualDownload = { shouldShowManualDownloadDialog = true },
onCancelDownload = { viewModel.cancelDownload(this) },
onUninstall = { AppInstaller.uninstall(context, packageName) },
@@ -192,6 +204,7 @@ private fun ScreenContentError(onNavigateUp: () -> Unit = {}) {
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun ScreenContentApp(
app: App,
isFavorite: Boolean = false,
isAnonymous: Boolean = true,
download: Download? = null,
installProgress: Float? = null,
@@ -203,13 +216,17 @@ private fun ScreenContentApp(
onNavigateToAppDetails: (packageName: String) -> Unit = {},
onDownload: () -> Unit = {},
onManualDownload: () -> Unit = {},
onFavorite: () -> Unit = {},
onCancelDownload: () -> Unit = {},
onUninstall: () -> Unit = {},
onOpen: () -> Unit = {},
onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {}
) {
val context = LocalContext.current
val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator<Screen>()
val coroutineScope = rememberCoroutineScope()
val shouldShowMenuOnMainPane = scaffoldNavigator
.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden
fun showMainPane() {
coroutineScope.launch {
@@ -223,31 +240,48 @@ private fun ScreenContentApp(
}
}
@Composable
fun SetupMenu() {
AppDetailsMenu(isInstalled = app.isInstalled, isFavorite = isFavorite) { menuItem ->
when (menuItem) {
AppDetailsMenuItem.FAVORITE -> onFavorite()
AppDetailsMenuItem.MANUAL_DOWNLOAD -> onManualDownload()
AppDetailsMenuItem.SHARE -> context.share(app)
AppDetailsMenuItem.APP_INFO -> context.appInfo(app.packageName)
AppDetailsMenuItem.PLAY_STORE -> context.browse("$SHARE_URL${app.packageName}")
AppDetailsMenuItem.ADD_TO_HOME -> {
ShortcutManagerUtil.requestPinShortcut(context, app.packageName)
}
}
}
}
NavigableSupportingPaneScaffold(
navigator = scaffoldNavigator,
mainPane = {
AnimatedPane {
ScreenContentAppMainPane(
app = app,
isAnonymous = isAnonymous,
download = download,
installProgress = installProgress,
isAnonymous = isAnonymous,
plexusScores = plexusScores,
dataSafetyReport = dataSafetyReport,
exodusReport = exodusReport,
hasValidUpdate = hasValidUpdate,
onNavigateUp = onNavigateUp,
onNavigateToDetailsDevProfile = { showExtraPane(Screen.DevProfile(it)) },
onNavigateToDetailsMore = { showExtraPane(Screen.DetailsMore) },
onNavigateToDetailsScreenshot = { showExtraPane(Screen.DetailsScreenshot(it)) },
onNavigateToDetailsReview = { showExtraPane(Screen.DetailsReview) },
onNavigateToDetailsExodus = { showExtraPane(Screen.DetailsExodus) },
onDownload = onDownload,
onManualDownload = onManualDownload,
onCancelDownload = onCancelDownload,
onUninstall = onUninstall,
onOpen = onOpen,
onNavigateToDetailsDevProfile = { showExtraPane(Screen.DevProfile(it)) },
onNavigateToDetailsMore = { showExtraPane(Screen.DetailsMore) },
onNavigateToDetailsReview = { showExtraPane(Screen.DetailsReview) },
onNavigateToDetailsExodus = { showExtraPane(Screen.DetailsExodus) },
onNavigateToDetailsScreenshot = { showExtraPane(Screen.DetailsScreenshot(it)) },
onTestingSubscriptionChange = onTestingSubscriptionChange
onTestingSubscriptionChange = onTestingSubscriptionChange,
menuActions = { if (shouldShowMenuOnMainPane) SetupMenu() }
)
}
},
@@ -255,7 +289,8 @@ private fun ScreenContentApp(
AnimatedPane {
DetailsSuggestions(
onNavigateUp = null,
onNavigateToAppDetails = onNavigateToAppDetails
onNavigateToAppDetails = onNavigateToAppDetails,
menuActions = { SetupMenu() }
)
}
},
@@ -313,7 +348,8 @@ private fun ScreenContentAppMainPane(
onCancelDownload: () -> Unit,
onUninstall: () -> Unit,
onOpen: () -> Unit,
onTestingSubscriptionChange: (subscribe: Boolean) -> Unit
onTestingSubscriptionChange: (subscribe: Boolean) -> Unit,
menuActions: @Composable (RowScope.() -> Unit) = {}
) {
val context = LocalContext.current
@@ -366,9 +402,7 @@ private fun ScreenContentAppMainPane(
Scaffold(
topBar = {
TopAppBarComposable(
onNavigateUp = onNavigateUp
)
TopAppBarComposable(onNavigateUp = onNavigateUp, actions = menuActions)
}
) { paddingValues ->
Column(

View File

@@ -7,6 +7,7 @@ package com.aurora.store.compose.ui.details
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@@ -44,6 +45,7 @@ import kotlin.random.Random
@Composable
fun DetailsSuggestions(
onNavigateUp: (() -> Unit)?,
menuActions: @Composable (RowScope.() -> Unit) = {},
viewModel: AppDetailsViewModel = hiltViewModel(),
onNavigateToAppDetails: (packageName: String) -> Unit
) {
@@ -51,6 +53,7 @@ fun DetailsSuggestions(
ScreenContent(
onNavigateUp = onNavigateUp,
menuActions = menuActions,
suggestions = suggestions,
onNavigateToAppDetails = onNavigateToAppDetails
)
@@ -59,11 +62,12 @@ fun DetailsSuggestions(
@Composable
private fun ScreenContent(
onNavigateUp: (() -> Unit)? = {},
menuActions: @Composable (RowScope.() -> Unit) = {},
suggestions: List<App> = emptyList(),
onNavigateToAppDetails: (packageName: String) -> Unit = {}
) {
Scaffold(
topBar = { TopAppBarComposable(onNavigateUp = onNavigateUp) }
topBar = { TopAppBarComposable(onNavigateUp = onNavigateUp, actions = menuActions) }
) { paddingValues ->
Column(
modifier = Modifier