compose: add NetworkSheet, MoreSheet and AppMenuSheet

NetworkSheet replaces the legacy NetworkDialogSheet with a persistent
modal that reacts to NetworkProvider status. MoreSheet hosts the
account / settings / about entry points. AppMenuSheet provides the
per-app long-click menu (blacklist, uninstall, export, info).
This commit is contained in:
Rahul Patel
2026-05-19 12:12:23 +05:30
parent 9e0a4ffee7
commit 0c60df635f
3 changed files with 446 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.commons
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.aurora.store.R
import com.aurora.store.compose.navigation.Destination
import com.aurora.store.viewmodel.commons.MoreViewModel
private data class MoreItem(
@StringRes val titleRes: Int,
@DrawableRes val iconRes: Int,
val onClick: () -> Unit
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MoreSheet(
onDismiss: () -> Unit,
onNavigateTo: (Destination) -> Unit = {},
viewModel: MoreViewModel = hiltViewModel()
) {
val mainItems = listOf(
MoreItem(R.string.title_apps_games, R.drawable.ic_apps) {
onNavigateTo(Destination.Installed)
},
MoreItem(R.string.title_blacklist_manager, R.drawable.ic_blacklist) {
onNavigateTo(Destination.Blacklist)
},
MoreItem(R.string.title_favourites_manager, R.drawable.ic_favorite_unchecked) {
onNavigateTo(Destination.Favourite)
},
MoreItem(R.string.title_spoof_manager, R.drawable.ic_spoof) {
onNavigateTo(Destination.Spoof)
}
)
val extraItems = listOf(
MoreItem(R.string.title_settings, R.drawable.ic_menu_settings) {
onNavigateTo(Destination.Settings)
},
MoreItem(R.string.title_about, R.drawable.ic_menu_about) { onNavigateTo(Destination.About) }
)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
) {
AccountHeader(
viewModel = viewModel,
onNavigateToAccounts = {
onNavigateTo(Destination.Accounts)
onDismiss()
}
)
HorizontalDivider()
Column(
Modifier.padding(
horizontal = dimensionResource(R.dimen.padding_small),
vertical = dimensionResource(R.dimen.padding_small)
)
) {
mainItems.forEach { item ->
NavigationDrawerItem(
icon = {
Icon(
painter = painterResource(item.iconRes),
contentDescription = null
)
},
label = { Text(stringResource(item.titleRes)) },
selected = false,
onClick = {
item.onClick()
onDismiss()
}
)
}
}
HorizontalDivider()
Column(
Modifier.padding(
horizontal = dimensionResource(R.dimen.padding_small),
vertical = dimensionResource(R.dimen.padding_small)
)
) {
extraItems.forEach { item ->
NavigationDrawerItem(
icon = {
Icon(
painter = painterResource(item.iconRes),
contentDescription = null
)
},
label = { Text(stringResource(item.titleRes)) },
selected = false,
onClick = {
item.onClick()
onDismiss()
}
)
}
}
Spacer(Modifier.navigationBarsPadding())
}
}
@Composable
private fun AccountHeader(viewModel: MoreViewModel, onNavigateToAccounts: () -> Unit) {
val context = LocalContext.current
val isAnonymous = viewModel.authProvider.isAnonymous
Row(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_large)),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = ImageRequest.Builder(context)
.data(viewModel.authProvider.authData?.userProfile?.artwork?.url)
.crossfade(true)
.build(),
contentDescription = null,
placeholder = painterResource(R.drawable.ic_account),
contentScale = ContentScale.Crop,
modifier = Modifier
.requiredSize(40.dp)
.clip(CircleShape)
)
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = dimensionResource(R.dimen.padding_normal))
) {
Text(
text = if (isAnonymous) {
stringResource(R.string.account_anonymous)
} else {
viewModel.authProvider.authData?.userProfile?.name
?: stringResource(R.string.status_unavailable)
},
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = viewModel.authProvider.authData?.userProfile?.email
?: stringResource(R.string.status_unavailable),
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
IconButton(onClick = onNavigateToAccounts) {
Icon(
painter = painterResource(R.drawable.ic_account_manager),
contentDescription = stringResource(R.string.manage_account)
)
}
}
}

View File

@@ -0,0 +1,96 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.commons
import android.content.ActivityNotFoundException
import android.content.Intent
import android.provider.Settings
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.aurora.extensions.isQAndAbove
import com.aurora.store.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NetworkSheet() {
val context = LocalContext.current
ModalBottomSheet(
onDismissRequest = {},
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
confirmValueChange = { it != SheetValue.Hidden }
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(R.dimen.padding_xlarge))
.padding(bottom = dimensionResource(R.dimen.padding_large)),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
painter = painterResource(R.drawable.ic_network),
contentDescription = null,
modifier = Modifier.requiredSize(48.dp)
)
Spacer(Modifier.padding(top = dimensionResource(R.dimen.padding_large)))
Text(
text = stringResource(R.string.title_no_network),
style = MaterialTheme.typography.titleLarge
)
Spacer(Modifier.padding(top = dimensionResource(R.dimen.padding_small)))
Text(
text = stringResource(R.string.check_connectivity),
style = MaterialTheme.typography.bodyMedium
)
Spacer(Modifier.padding(top = dimensionResource(R.dimen.padding_xlarge)))
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
try {
if (isQAndAbove) {
context.startActivity(
Intent(Settings.Panel.ACTION_INTERNET_CONNECTIVITY)
)
} else {
context.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
}
} catch (e: ActivityNotFoundException) {
Log.i("NetworkSheet", "Unable to launch network settings")
try {
context.startActivity(Intent(Settings.ACTION_SETTINGS))
} catch (_: Exception) { }
}
}
) {
Text(stringResource(R.string.action_check))
}
Spacer(Modifier.navigationBarsPadding())
}
}
}

View File

@@ -0,0 +1,148 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.sheets
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.aurora.extensions.openInfo
import com.aurora.extensions.toast
import com.aurora.store.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.MinimalApp
import com.aurora.store.util.PackageUtil
import com.aurora.store.viewmodel.sheets.AppMenuViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppMenuSheet(
app: MinimalApp,
onDismiss: () -> Unit,
viewModel: AppMenuViewModel = hiltViewModel()
) {
val context = LocalContext.current
val isBlacklisted = viewModel.blacklistProvider.isBlacklisted(app.packageName)
val isInstalled = PackageUtil.isInstalled(context, app.packageName)
val exportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/zip")
) { uri ->
if (uri != null) {
viewModel.copyInstalledApp(context, app, uri)
} else {
context.toast(R.string.failed_apk_export)
}
onDismiss()
}
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
) {
NavigationDrawerItem(
icon = {
Icon(
painter = painterResource(
if (isBlacklisted) R.drawable.ic_cancel else R.drawable.ic_blacklist
),
contentDescription = null
)
},
label = {
val labelRes = if (isBlacklisted) {
R.string.action_whitelist
} else {
R.string.action_blacklist_add
}
Text(stringResource(labelRes))
},
selected = false,
onClick = {
if (isBlacklisted) {
viewModel.blacklistProvider.whitelist(app.packageName)
context.toast(R.string.toast_apk_whitelisted)
} else {
viewModel.blacklistProvider.blacklist(app.packageName)
context.toast(R.string.toast_apk_blacklisted)
}
AuroraApp.events.send(BusEvent.Blacklisted(app.packageName))
onDismiss()
},
colors = NavigationDrawerItemDefaults.colors(
unselectedContainerColor = androidx.compose.ui.graphics.Color.Transparent
)
)
HorizontalDivider()
if (isInstalled) {
NavigationDrawerItem(
icon = {
Icon(
painter = painterResource(R.drawable.ic_delete_forever),
contentDescription = null
)
},
label = { Text(stringResource(R.string.action_uninstall)) },
selected = false,
onClick = {
AppInstaller.uninstall(context, app.packageName)
onDismiss()
},
colors = NavigationDrawerItemDefaults.colors(
unselectedContainerColor = androidx.compose.ui.graphics.Color.Transparent
)
)
NavigationDrawerItem(
icon = {
Icon(
painter = painterResource(R.drawable.ic_file_copy),
contentDescription = null
)
},
label = { Text(stringResource(R.string.action_export)) },
selected = false,
onClick = { exportLauncher.launch("${app.packageName}.zip") },
colors = NavigationDrawerItemDefaults.colors(
unselectedContainerColor = androidx.compose.ui.graphics.Color.Transparent
)
)
}
NavigationDrawerItem(
icon = {
Icon(
painter = painterResource(R.drawable.ic_about),
contentDescription = null
)
},
label = { Text(stringResource(R.string.action_info)) },
selected = false,
onClick = {
context.openInfo(app.packageName)
onDismiss()
},
colors = NavigationDrawerItemDefaults.colors(
unselectedContainerColor = androidx.compose.ui.graphics.Color.Transparent
)
)
HorizontalDivider()
androidx.compose.foundation.layout.Spacer(Modifier.navigationBarsPadding())
}
}