compose: details: Move manual download to independent screen

We want to allow users to select location to download files and select splits
in future too.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-05-20 22:52:17 +08:00
parent 38f96eacbc
commit 70d68dc872
4 changed files with 202 additions and 137 deletions

View File

@@ -66,4 +66,10 @@ sealed class Screen(
*/
@Serializable
data object DetailsPermission : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data object DetailsManualDownload : Screen()
}

View File

@@ -28,10 +28,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -75,7 +72,6 @@ import com.aurora.store.compose.ui.details.components.AppScreenshots
import com.aurora.store.compose.ui.details.components.AppTags
import com.aurora.store.compose.ui.details.components.AppTesting
import com.aurora.store.compose.ui.dev.DevProfileScreen
import com.aurora.store.compose.ui.dialogs.ManualDownloadDialog
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.Report
import com.aurora.store.data.model.Scores
@@ -107,33 +103,7 @@ fun AppDetailsScreen(
val installProgress by viewModel.installProgress.collectAsStateWithLifecycle()
val suggestions by viewModel.suggestions.collectAsStateWithLifecycle()
var shouldShowManualDownloadDialog by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(key1 = Unit) { viewModel.fetchAppDetails(packageName) }
LaunchedEffect(key1 = Unit) {
viewModel.purchaseStatus.collect { success ->
if (shouldShowManualDownloadDialog) {
if (success) {
shouldShowManualDownloadDialog = false
context.toast(R.string.toast_manual_available)
} else {
context.toast(R.string.toast_manual_unavailable)
}
} else {
context.toast(R.string.download_failed)
}
}
}
if (shouldShowManualDownloadDialog) {
ManualDownloadDialog(
currentVersionCode = app!!.versionCode,
onConfirm = { versionCode ->
viewModel.purchase(app!!.copy(versionCode = versionCode))
},
onDismiss = { shouldShowManualDownloadDialog = false }
)
}
with(app) {
when {
@@ -155,9 +125,8 @@ fun AppDetailsScreen(
hasValidUpdate = viewModel.hasValidUpdate,
onNavigateUp = onNavigateUp,
onNavigateToAppDetails = onNavigateToAppDetails,
onDownload = { viewModel.download(this) },
onDownload = { viewModel.purchase(this) },
onFavorite = { viewModel.toggleFavourite(this) },
onManualDownload = { shouldShowManualDownloadDialog = true },
onCancelDownload = { viewModel.cancelDownload(this) },
onUninstall = { AppInstaller.uninstall(context, packageName) },
onOpen = {
@@ -230,7 +199,6 @@ private fun ScreenContentApp(
onNavigateUp: () -> Unit = {},
onNavigateToAppDetails: (packageName: String) -> Unit = {},
onDownload: () -> Unit = {},
onManualDownload: () -> Unit = {},
onFavorite: () -> Unit = {},
onCancelDownload: () -> Unit = {},
onUninstall: () -> Unit = {},
@@ -260,7 +228,9 @@ private fun ScreenContentApp(
AppDetailsMenu(isInstalled = app.isInstalled, isFavorite = isFavorite) { menuItem ->
when (menuItem) {
AppDetailsMenuItem.FAVORITE -> onFavorite()
AppDetailsMenuItem.MANUAL_DOWNLOAD -> onManualDownload()
AppDetailsMenuItem.MANUAL_DOWNLOAD -> {
showExtraPane(Screen.DetailsManualDownload)
}
AppDetailsMenuItem.SHARE -> context.share(app)
AppDetailsMenuItem.APP_INFO -> context.appInfo(app.packageName)
AppDetailsMenuItem.PLAY_STORE -> context.browse("$SHARE_URL${app.packageName}")
@@ -293,7 +263,7 @@ private fun ScreenContentApp(
onNavigateToDetailsExodus = { showExtraPane(Screen.DetailsExodus) },
onNavigateToDetailsPermission = { showExtraPane(Screen.DetailsPermission) },
onDownload = onDownload,
onManualDownload = onManualDownload,
onManualDownload = { showExtraPane(Screen.DetailsManualDownload) },
onCancelDownload = onCancelDownload,
onUninstall = onUninstall,
onOpen = onOpen,
@@ -328,6 +298,9 @@ private fun ScreenContentApp(
index = screen.index,
onNavigateUp = ::showMainPane
)
is Screen.DetailsManualDownload -> DetailsManualDownloadScreen(
onNavigateUp = ::showMainPane
)
is Screen.DevProfile -> DevProfileScreen(
publisherId = app.developerName,
onNavigateUp = ::showMainPane,

View File

@@ -0,0 +1,188 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.details
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.core.text.isDigitsOnly
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.core.layout.WindowWidthSizeClass
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composables.InfoComposable
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.compose.composables.preview.AppPreviewProvider
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import kotlinx.coroutines.android.awaitFrame
@Composable
fun DetailsManualDownloadScreen(
onNavigateUp: () -> Unit,
viewModel: AppDetailsViewModel = hiltViewModel(),
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val context = LocalContext.current
val app by viewModel.app.collectAsStateWithLifecycle()
val topAppBarTitle = when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> app!!.displayName
else -> stringResource(R.string.title_manual_download)
}
LaunchedEffect(key1 = Unit) {
viewModel.purchaseStatus.collect { success ->
if (success) {
context.toast(R.string.toast_manual_available)
onNavigateUp()
} else {
context.toast(R.string.toast_manual_unavailable)
}
}
}
ScreenContent(
topAppBarTitle = topAppBarTitle,
currentVersionCode = app!!.versionCode,
onNavigateUp = onNavigateUp,
onDownload = { versionCode ->
viewModel.purchase(app!!.copy(versionCode = versionCode))
}
)
}
@Composable
private fun ScreenContent(
topAppBarTitle: String? = null,
currentVersionCode: Long = 0L,
onNavigateUp: () -> Unit = {},
onDownload: (versionCode: Long) -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val focusRequester = remember { FocusRequester() }
var versionCode by remember {
val initText = currentVersionCode.toString()
mutableStateOf(TextFieldValue(text = initText, selection = TextRange(initText.length)))
}
LaunchedEffect(focusRequester) {
awaitFrame()
focusRequester.requestFocus()
}
Scaffold(
topBar = {
TopAppBarComposable(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) {
InfoComposable(
icon = R.drawable.ic_download_manager,
title = AnnotatedString(text = stringResource(R.string.manual_download_hint))
)
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
value = versionCode,
onValueChange = { if (it.text.isDigitsOnly()) versionCode = it },
shape = RoundedCornerShape(10.dp),
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
) {
FilledTonalButton(
modifier = Modifier.weight(1F),
onClick = onNavigateUp
) {
Text(
text = stringResource(R.string.action_cancel),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Button(
modifier = Modifier.weight(1F),
onClick = { onDownload(versionCode.text.toLong()) }) {
Text(
text = stringResource(R.string.action_install),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
}
@Preview
@Composable
private fun DetailsManualDownloadScreenPreview(
@PreviewParameter(AppPreviewProvider::class) app: App
) {
ScreenContent(
topAppBarTitle = app.displayName,
currentVersionCode = app.versionCode
)
}

View File

@@ -1,102 +0,0 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.dialogs
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.core.text.isDigitsOnly
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composables.preview.AppPreviewProvider
import kotlinx.coroutines.android.awaitFrame
/**
* Dialog for entering the versionCode manually for an App to download
* @param currentVersionCode Current versionCode of the app
* @param onConfirm Callback when the versionCode has been entered
* @param onDismiss Callback when the dialog has been dismissed
*/
@Composable
fun ManualDownloadDialog(
currentVersionCode: Long,
onConfirm: (versionCode: Long) -> Unit = {},
onDismiss: () -> Unit = {}
) {
val focusRequester = remember { FocusRequester() }
var versionCode by remember {
val initText = currentVersionCode.toString()
mutableStateOf(TextFieldValue(text = initText, selection = TextRange(initText.length)))
}
LaunchedEffect(focusRequester) {
awaitFrame()
focusRequester.requestFocus()
}
AlertDialog(
title = { Text(text = stringResource(R.string.title_manual_download)) },
text = {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(text = stringResource(R.string.manual_download_hint))
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
value = versionCode,
onValueChange = { if (it.text.isDigitsOnly()) versionCode = it },
shape = RoundedCornerShape(10.dp),
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
)
)
}
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton (onClick = { onConfirm(versionCode.text.toLong()) }) {
Text(text = stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(text = stringResource(android.R.string.cancel))
}
}
)
}
@Preview
@Composable
private fun ManualDownloadDialogPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
ManualDownloadDialog(currentVersionCode = app.versionCode)
}