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.Bundle
import android.os.Environment import android.os.Environment
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings
import android.util.Log import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater 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) { fun Context.share(app: App) {
try { try {
val sendIntent = Intent().apply { 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 android.content.ActivityNotFoundException
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.RowScope
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.rememberScrollState import androidx.compose.foundation.rememberScrollState
@@ -15,6 +16,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.AnimatedPane 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.layout.SupportingPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.NavigableSupportingPaneScaffold import androidx.compose.material3.adaptive.navigation.NavigableSupportingPaneScaffold
import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator
@@ -37,6 +39,10 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.annotation.ExperimentalCoilApi import coil3.annotation.ExperimentalCoilApi
import coil3.compose.LocalAsyncImagePreviewHandler 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.extensions.toast
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.R 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.app.NoAppComposable
import com.aurora.store.compose.composables.preview.AppPreviewProvider import com.aurora.store.compose.composables.preview.AppPreviewProvider
import com.aurora.store.compose.composables.preview.coilPreviewProvider import com.aurora.store.compose.composables.preview.coilPreviewProvider
import com.aurora.store.compose.menu.AppDetailsMenu
import com.aurora.store.compose.menu.items.AppDetailsMenuItem
import com.aurora.store.compose.navigation.Screen import com.aurora.store.compose.navigation.Screen
import com.aurora.store.compose.ui.details.components.AppActions import com.aurora.store.compose.ui.details.components.AppActions
import com.aurora.store.compose.ui.details.components.AppChangelog 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.data.room.download.Download
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import com.aurora.store.util.PackageUtil.PACKAGE_NAME_GMS import com.aurora.store.util.PackageUtil.PACKAGE_NAME_GMS
import com.aurora.store.util.ShortcutManagerUtil
import com.aurora.store.viewmodel.details.AppDetailsViewModel import com.aurora.store.viewmodel.details.AppDetailsViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
@@ -80,6 +89,7 @@ fun AppDetailsScreen(
val context = LocalContext.current val context = LocalContext.current
val app by viewModel.app.collectAsStateWithLifecycle() val app by viewModel.app.collectAsStateWithLifecycle()
val favorite by viewModel.favourite.collectAsStateWithLifecycle()
val exodusReport by viewModel.exodusReport.collectAsStateWithLifecycle() val exodusReport by viewModel.exodusReport.collectAsStateWithLifecycle()
val dataSafetyReport by viewModel.dataSafetyReport.collectAsStateWithLifecycle() val dataSafetyReport by viewModel.dataSafetyReport.collectAsStateWithLifecycle()
val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle() val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle()
@@ -122,6 +132,7 @@ fun AppDetailsScreen(
} else { } else {
ScreenContentApp( ScreenContentApp(
app = this, app = this,
isFavorite = favorite,
isAnonymous = viewModel.authProvider.isAnonymous, isAnonymous = viewModel.authProvider.isAnonymous,
download = download, download = download,
installProgress = installProgress, installProgress = installProgress,
@@ -132,6 +143,7 @@ fun AppDetailsScreen(
onNavigateUp = onNavigateUp, onNavigateUp = onNavigateUp,
onNavigateToAppDetails = onNavigateToAppDetails, onNavigateToAppDetails = onNavigateToAppDetails,
onDownload = { viewModel.download(this) }, onDownload = { viewModel.download(this) },
onFavorite = { viewModel.toggleFavourite(this) },
onManualDownload = { shouldShowManualDownloadDialog = true }, onManualDownload = { shouldShowManualDownloadDialog = true },
onCancelDownload = { viewModel.cancelDownload(this) }, onCancelDownload = { viewModel.cancelDownload(this) },
onUninstall = { AppInstaller.uninstall(context, packageName) }, onUninstall = { AppInstaller.uninstall(context, packageName) },
@@ -192,6 +204,7 @@ private fun ScreenContentError(onNavigateUp: () -> Unit = {}) {
@OptIn(ExperimentalMaterial3AdaptiveApi::class) @OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun ScreenContentApp( private fun ScreenContentApp(
app: App, app: App,
isFavorite: Boolean = false,
isAnonymous: Boolean = true, isAnonymous: Boolean = true,
download: Download? = null, download: Download? = null,
installProgress: Float? = null, installProgress: Float? = null,
@@ -203,13 +216,17 @@ private fun ScreenContentApp(
onNavigateToAppDetails: (packageName: String) -> Unit = {}, onNavigateToAppDetails: (packageName: String) -> Unit = {},
onDownload: () -> Unit = {}, onDownload: () -> Unit = {},
onManualDownload: () -> Unit = {}, onManualDownload: () -> Unit = {},
onFavorite: () -> Unit = {},
onCancelDownload: () -> Unit = {}, onCancelDownload: () -> Unit = {},
onUninstall: () -> Unit = {}, onUninstall: () -> Unit = {},
onOpen: () -> Unit = {}, onOpen: () -> Unit = {},
onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {} onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {}
) { ) {
val context = LocalContext.current
val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator<Screen>() val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator<Screen>()
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val shouldShowMenuOnMainPane = scaffoldNavigator
.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden
fun showMainPane() { fun showMainPane() {
coroutineScope.launch { 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( NavigableSupportingPaneScaffold(
navigator = scaffoldNavigator, navigator = scaffoldNavigator,
mainPane = { mainPane = {
AnimatedPane { AnimatedPane {
ScreenContentAppMainPane( ScreenContentAppMainPane(
app = app, app = app,
isAnonymous = isAnonymous,
download = download, download = download,
installProgress = installProgress, installProgress = installProgress,
isAnonymous = isAnonymous,
plexusScores = plexusScores, plexusScores = plexusScores,
dataSafetyReport = dataSafetyReport, dataSafetyReport = dataSafetyReport,
exodusReport = exodusReport, exodusReport = exodusReport,
hasValidUpdate = hasValidUpdate, hasValidUpdate = hasValidUpdate,
onNavigateUp = onNavigateUp, 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, onDownload = onDownload,
onManualDownload = onManualDownload, onManualDownload = onManualDownload,
onCancelDownload = onCancelDownload, onCancelDownload = onCancelDownload,
onUninstall = onUninstall, onUninstall = onUninstall,
onOpen = onOpen, onOpen = onOpen,
onNavigateToDetailsDevProfile = { showExtraPane(Screen.DevProfile(it)) }, onTestingSubscriptionChange = onTestingSubscriptionChange,
onNavigateToDetailsMore = { showExtraPane(Screen.DetailsMore) }, menuActions = { if (shouldShowMenuOnMainPane) SetupMenu() }
onNavigateToDetailsReview = { showExtraPane(Screen.DetailsReview) },
onNavigateToDetailsExodus = { showExtraPane(Screen.DetailsExodus) },
onNavigateToDetailsScreenshot = { showExtraPane(Screen.DetailsScreenshot(it)) },
onTestingSubscriptionChange = onTestingSubscriptionChange
) )
} }
}, },
@@ -255,7 +289,8 @@ private fun ScreenContentApp(
AnimatedPane { AnimatedPane {
DetailsSuggestions( DetailsSuggestions(
onNavigateUp = null, onNavigateUp = null,
onNavigateToAppDetails = onNavigateToAppDetails onNavigateToAppDetails = onNavigateToAppDetails,
menuActions = { SetupMenu() }
) )
} }
}, },
@@ -313,7 +348,8 @@ private fun ScreenContentAppMainPane(
onCancelDownload: () -> Unit, onCancelDownload: () -> Unit,
onUninstall: () -> Unit, onUninstall: () -> Unit,
onOpen: () -> Unit, onOpen: () -> Unit,
onTestingSubscriptionChange: (subscribe: Boolean) -> Unit onTestingSubscriptionChange: (subscribe: Boolean) -> Unit,
menuActions: @Composable (RowScope.() -> Unit) = {}
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -366,9 +402,7 @@ private fun ScreenContentAppMainPane(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBarComposable( TopAppBarComposable(onNavigateUp = onNavigateUp, actions = menuActions)
onNavigateUp = onNavigateUp
)
} }
) { paddingValues -> ) { paddingValues ->
Column( 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
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
@@ -44,6 +45,7 @@ import kotlin.random.Random
@Composable @Composable
fun DetailsSuggestions( fun DetailsSuggestions(
onNavigateUp: (() -> Unit)?, onNavigateUp: (() -> Unit)?,
menuActions: @Composable (RowScope.() -> Unit) = {},
viewModel: AppDetailsViewModel = hiltViewModel(), viewModel: AppDetailsViewModel = hiltViewModel(),
onNavigateToAppDetails: (packageName: String) -> Unit onNavigateToAppDetails: (packageName: String) -> Unit
) { ) {
@@ -51,6 +53,7 @@ fun DetailsSuggestions(
ScreenContent( ScreenContent(
onNavigateUp = onNavigateUp, onNavigateUp = onNavigateUp,
menuActions = menuActions,
suggestions = suggestions, suggestions = suggestions,
onNavigateToAppDetails = onNavigateToAppDetails onNavigateToAppDetails = onNavigateToAppDetails
) )
@@ -59,11 +62,12 @@ fun DetailsSuggestions(
@Composable @Composable
private fun ScreenContent( private fun ScreenContent(
onNavigateUp: (() -> Unit)? = {}, onNavigateUp: (() -> Unit)? = {},
menuActions: @Composable (RowScope.() -> Unit) = {},
suggestions: List<App> = emptyList(), suggestions: List<App> = emptyList(),
onNavigateToAppDetails: (packageName: String) -> Unit = {} onNavigateToAppDetails: (packageName: String) -> Unit = {}
) { ) {
Scaffold( Scaffold(
topBar = { TopAppBarComposable(onNavigateUp = onNavigateUp) } topBar = { TopAppBarComposable(onNavigateUp = onNavigateUp, actions = menuActions) }
) { paddingValues -> ) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier