diff --git a/app/src/main/java/com/aurora/store/compose/ui/commons/MoreSheet.kt b/app/src/main/java/com/aurora/store/compose/ui/commons/MoreSheet.kt new file mode 100644 index 000000000..e3a320588 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/commons/MoreSheet.kt @@ -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) + ) + } + } +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/commons/NetworkSheet.kt b/app/src/main/java/com/aurora/store/compose/ui/commons/NetworkSheet.kt new file mode 100644 index 000000000..5df746c89 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/commons/NetworkSheet.kt @@ -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()) + } + } +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/sheets/AppMenuSheet.kt b/app/src/main/java/com/aurora/store/compose/ui/sheets/AppMenuSheet.kt new file mode 100644 index 000000000..8a2ae2caa --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/sheets/AppMenuSheet.kt @@ -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()) + } +}