compose: build MainScreen and the apps/games tabs

MainScreen hosts the bottom-nav between Apps / Games / Updates with a
HorizontalPager. The Apps/Games host (AppsGamesScreen) carries the
For You / Top Charts / Categories sub-tabs implemented as separate
pages.
This commit is contained in:
Rahul Patel
2026-05-19 12:11:23 +05:30
parent baf593bd51
commit 8d3eb1176e
5 changed files with 759 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.apps
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.aurora.gplayapi.helpers.contracts.StreamContract
import com.aurora.gplayapi.helpers.contracts.TopChartsContract
import com.aurora.store.R
import com.aurora.store.compose.navigation.Destination
import com.aurora.store.data.model.MinimalApp
import com.aurora.store.util.Preferences
import com.aurora.store.viewmodel.category.CategoryViewModel
import com.aurora.store.viewmodel.homestream.StreamViewModel
import com.aurora.store.viewmodel.topchart.TopChartViewModel
import kotlinx.coroutines.launch
internal fun category(pageType: Int): StreamContract.Category =
if (pageType == 1) StreamContract.Category.GAME else StreamContract.Category.APPLICATION
private enum class AppsTab(@StringRes val titleRes: Int) {
FOR_YOU(R.string.tab_for_you),
TOP_CHARTS(R.string.tab_top_charts),
CATEGORIES(R.string.tab_categories)
}
@Composable
fun AppsGamesScreen(
pageType: Int,
streamViewModel: StreamViewModel = hiltViewModel(key = "stream_$pageType"),
topChartViewModel: TopChartViewModel = hiltViewModel(key = "topChart_$pageType"),
categoryViewModel: CategoryViewModel = hiltViewModel(key = "category_$pageType"),
onNavigateTo: (Destination) -> Unit = {}
) {
val context = LocalContext.current
val chartType = if (pageType == 1) {
TopChartsContract.Type.GAME
} else {
TopChartsContract.Type.APPLICATION
}
LaunchedEffect(Unit) {
topChartViewModel.getStreamCluster(chartType, TopChartsContract.Chart.TOP_SELLING_FREE)
}
val isForYouEnabled = Preferences.getBoolean(context, Preferences.PREFERENCE_FOR_YOU)
val tabs = buildList {
if (isForYouEnabled) add(AppsTab.FOR_YOU)
add(AppsTab.TOP_CHARTS)
add(AppsTab.CATEGORIES)
}
val pagerState = rememberPagerState { tabs.size }
val coroutineScope = rememberCoroutineScope()
Column(modifier = Modifier.fillMaxSize()) {
PrimaryTabRow(
modifier = Modifier.fillMaxWidth(),
selectedTabIndex = pagerState.currentPage
) {
tabs.forEachIndexed { index, tab ->
Tab(
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = { Text(stringResource(tab.titleRes)) }
)
}
}
HorizontalPager(
modifier = Modifier.fillMaxSize(),
state = pagerState,
verticalAlignment = Alignment.Top,
userScrollEnabled = false
) { page ->
when (tabs[page]) {
AppsTab.FOR_YOU -> ForYouContent(
pageType = pageType,
viewModel = streamViewModel,
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
onAppLongClick = { onNavigateTo(Destination.AppMenu(MinimalApp.fromApp(it))) },
onHeaderClick = { onNavigateTo(Destination.StreamBrowse(it)) },
onClusterScrolled = { cluster ->
streamViewModel.observeCluster(category(pageType), cluster)
},
onScrolledToEnd = {
streamViewModel.observe(category(pageType), StreamContract.Type.HOME)
}
)
AppsTab.TOP_CHARTS -> TopChartsContent(
pageType = pageType,
viewModel = topChartViewModel,
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) }
)
AppsTab.CATEGORIES -> CategoriesContent(
pageType = pageType,
viewModel = categoryViewModel,
onCategoryClick = { onNavigateTo(Destination.CategoryBrowse(it)) }
)
}
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.apps
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper
import com.aurora.gplayapi.data.models.Category
import com.aurora.store.CategoryStash
import com.aurora.store.compose.composable.CategoryItem
import com.aurora.store.compose.composable.ShimmerCategoryRow
import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.data.model.ViewState
import com.aurora.store.viewmodel.category.CategoryViewModel
@Composable
internal fun CategoriesContent(
pageType: Int,
viewModel: CategoryViewModel,
onCategoryClick: (Category) -> Unit
) {
val categoryType = if (pageType == 1) Category.Type.GAME else Category.Type.APPLICATION
val state by viewModel.liveData.observeAsState()
LaunchedEffect(categoryType) {
viewModel.getCategoryList(categoryType)
}
@Suppress("UNCHECKED_CAST")
val categories = (state as? ViewState.Success<*>)?.data as? CategoryStash
val list = categories?.get(categoryType)
CategoriesBody(list = list, onCategoryClick = onCategoryClick)
}
@Composable
private fun CategoriesBody(list: List<Category>?, onCategoryClick: (Category) -> Unit = {}) {
if (list.isNullOrEmpty()) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(10) { ShimmerCategoryRow() }
}
} else {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(count = list.size, key = { list[it].title }) { index ->
CategoryItem(
category = list[index],
onClick = { onCategoryClick(list[index]) }
)
}
}
}
}
@PreviewWrapper(ThemePreviewProvider::class)
@Preview(showBackground = true)
@Composable
private fun CategoriesBodyLoadingPreview() {
CategoriesBody(list = null)
}
@PreviewWrapper(ThemePreviewProvider::class)
@Preview(showBackground = true)
@Composable
private fun CategoriesBodyLoadedPreview() {
val categories = listOf(
"Art & Design",
"Auto & Vehicles",
"Beauty",
"Books & Reference",
"Business",
"Comics",
"Communication",
"Dating",
"Education",
"Entertainment"
).map { Category(title = it, imageUrl = "") }
CategoriesBody(list = categories)
}

View File

@@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.apps
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.contracts.StreamContract
import com.aurora.store.HomeStash
import com.aurora.store.compose.composable.StreamCarousel
import com.aurora.store.compose.composition.observeForced
import com.aurora.store.data.model.ViewState
import com.aurora.store.viewmodel.homestream.StreamViewModel
@Composable
internal fun ForYouContent(
pageType: Int,
viewModel: StreamViewModel,
onAppClick: (App) -> Unit,
onAppLongClick: (App) -> Unit,
onHeaderClick: (StreamCluster) -> Unit,
onClusterScrolled: (StreamCluster) -> Unit,
onScrolledToEnd: () -> Unit
) {
val category = category(pageType)
val state by viewModel.liveData.observeForced()
LaunchedEffect(category) {
viewModel.getStreamBundle(category, StreamContract.Type.HOME)
}
@Suppress("UNCHECKED_CAST")
val streamBundle = (state as? ViewState.Success<*>)?.data as? HomeStash
StreamCarousel(
modifier = Modifier.fillMaxSize(),
streamBundle = streamBundle?.get(category),
onHeaderClick = onHeaderClick,
onAppClick = onAppClick,
onAppLongClick = onAppLongClick,
onClusterScrolled = onClusterScrolled,
onScrolledToEnd = onScrolledToEnd
)
}

View File

@@ -0,0 +1,255 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.apps
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.SecondaryScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
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.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewWrapper
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.contracts.TopChartsContract
import com.aurora.store.R
import com.aurora.store.compose.composable.EmptyState
import com.aurora.store.compose.composable.ShimmerAppRow
import com.aurora.store.compose.composable.app.LargeAppListItem
import com.aurora.store.compose.composition.collectForced
import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.ThemePreviewProvider
import com.aurora.store.data.model.ViewState
import com.aurora.store.data.model.ViewState.Loading.getDataAs
import com.aurora.store.viewmodel.topchart.TopChartViewModel
private const val LOAD_MORE_THRESHOLD = 2
@Composable
internal fun TopChartsContent(
pageType: Int,
viewModel: TopChartViewModel,
onAppClick: (App) -> Unit
) {
val charts = listOf(
TopChartsContract.Chart.TOP_SELLING_FREE,
TopChartsContract.Chart.TOP_GROSSING,
TopChartsContract.Chart.MOVERS_SHAKERS,
TopChartsContract.Chart.TOP_SELLING_PAID
)
val chartTitles = listOf(
R.string.tab_top_free,
R.string.tab_top_grossing,
R.string.tab_trending,
R.string.tab_top_paid
)
val chartType =
if (pageType == 1) TopChartsContract.Type.GAME else TopChartsContract.Type.APPLICATION
var selectedIndex by rememberSaveable { mutableIntStateOf(0) }
val selectedChart = charts[selectedIndex]
val state by viewModel.state.collectForced(ViewState.Loading)
val cluster = state.getDataAs<StreamCluster?>()
val listState = rememberLazyListState()
LaunchedEffect(selectedIndex) {
listState.scrollToItem(0)
viewModel.getStreamCluster(chartType, selectedChart)
}
val reachedEnd by remember {
derivedStateOf {
val last = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
val total = listState.layoutInfo.totalItemsCount
last >= total - LOAD_MORE_THRESHOLD
}
}
LaunchedEffect(reachedEnd) {
if (reachedEnd && cluster?.hasNext() == true) {
viewModel.nextCluster(chartType, selectedChart)
}
}
TopChartsBody(
selectedIndex = selectedIndex,
chartTitles = chartTitles,
state = state,
cluster = cluster,
listState = listState,
onTabSelected = { selectedIndex = it },
onRetry = { viewModel.getStreamCluster(chartType, selectedChart) },
onAppClick = onAppClick
)
}
@Composable
private fun TopChartsBody(
selectedIndex: Int,
chartTitles: List<Int>,
state: ViewState,
cluster: StreamCluster?,
listState: LazyListState = rememberLazyListState(),
onTabSelected: (Int) -> Unit = {},
onRetry: () -> Unit = {},
onAppClick: (App) -> Unit = {}
) {
Column(
modifier = Modifier.fillMaxSize()
) {
SecondaryScrollableTabRow(
selectedTabIndex = selectedIndex,
edgePadding = dimensionResource(R.dimen.padding_small)
) {
chartTitles.forEachIndexed { index, titleRes ->
Tab(
selected = selectedIndex == index,
onClick = { onTabSelected(index) },
text = { Text(stringResource(titleRes)) }
)
}
}
when {
state is ViewState.Error -> EmptyState(
modifier = Modifier.weight(1f),
icon = R.drawable.ic_apps,
message = R.string.no_apps_available,
actionLabel = R.string.action_retry,
onAction = onRetry
)
cluster != null && cluster.clusterAppList.isEmpty() -> EmptyState(
modifier = Modifier.weight(1f),
icon = R.drawable.ic_apps,
message = R.string.no_apps_available
)
cluster != null -> {
val apps = cluster.clusterAppList
LazyColumn(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
state = listState,
contentPadding = PaddingValues(
vertical = dimensionResource(R.dimen.padding_small)
),
verticalArrangement = Arrangement.spacedBy(
dimensionResource(R.dimen.margin_xsmall)
)
) {
items(count = apps.size, key = { apps[it].id }) { index ->
LargeAppListItem(
app = apps[index],
onClick = { onAppClick(apps[index]) }
)
}
if (cluster.hasNext()) {
item(key = "progress") {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_large)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
}
}
}
else -> LazyColumn(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
contentPadding = PaddingValues(vertical = dimensionResource(R.dimen.padding_small)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xsmall))
) {
items(8) { ShimmerAppRow() }
}
}
}
}
private val previewChartTitles = listOf(
R.string.tab_top_free,
R.string.tab_top_grossing,
R.string.tab_trending,
R.string.tab_top_paid
)
@PreviewWrapper(ThemePreviewProvider::class)
@Preview(showBackground = true)
@Composable
private fun TopChartsBodyLoadingPreview() {
TopChartsBody(
selectedIndex = 0,
chartTitles = previewChartTitles,
state = ViewState.Loading,
cluster = null
)
}
@PreviewWrapper(ThemePreviewProvider::class)
@Preview(showBackground = true)
@Composable
private fun TopChartsBodyLoadedPreview() {
val app = AppPreviewProvider().values.first()
val apps = List(5) { i -> app.copy(id = i + 1, packageName = "com.preview.app$i") }
val cluster = StreamCluster(clusterAppList = apps)
TopChartsBody(
selectedIndex = 0,
chartTitles = previewChartTitles,
state = ViewState.Success(cluster),
cluster = cluster
)
}
@PreviewWrapper(ThemePreviewProvider::class)
@Preview(showBackground = true)
@Composable
private fun TopChartsBodyEmptyPreview() {
val cluster = StreamCluster(clusterAppList = emptyList())
TopChartsBody(
selectedIndex = 1,
chartTitles = previewChartTitles,
state = ViewState.Success(cluster),
cluster = cluster
)
}
@PreviewWrapper(ThemePreviewProvider::class)
@Preview(showBackground = true)
@Composable
private fun TopChartsBodyErrorPreview() {
TopChartsBody(
selectedIndex = 0,
chartTitles = previewChartTitles,
state = ViewState.Error("Network error"),
cluster = null
)
}

View File

@@ -0,0 +1,240 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.main
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
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.rememberCoroutineScope
import androidx.compose.runtime.setValue
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aurora.extensions.requiresObbDir
import com.aurora.store.MainViewModel
import com.aurora.store.R
import com.aurora.store.compose.composable.TopAppBar
import com.aurora.store.compose.navigation.Destination
import com.aurora.store.compose.ui.apps.AppsGamesScreen
import com.aurora.store.compose.ui.commons.MoreSheet
import com.aurora.store.compose.ui.commons.NetworkSheet
import com.aurora.store.compose.ui.sheets.AppMenuSheet
import com.aurora.store.compose.ui.updates.UpdatesScreen
import com.aurora.store.data.model.MinimalApp
import com.aurora.store.data.model.NetworkStatus
import com.aurora.store.data.model.PermissionType
import com.aurora.store.data.providers.PermissionProvider.Companion.isGranted
import com.aurora.store.viewmodel.all.UpdatesViewModel
import kotlinx.coroutines.launch
private enum class MainTab(
@StringRes val labelRes: Int,
@DrawableRes val iconRes: Int
) {
APPS(R.string.title_apps, R.drawable.ic_apps),
GAMES(R.string.title_games, R.drawable.ic_games),
UPDATES(R.string.title_updates, R.drawable.ic_updates)
}
@Composable
fun MainScreen(
initialTab: Int = 0,
mainViewModel: MainViewModel = hiltViewModel(),
updatesViewModel: UpdatesViewModel = hiltViewModel(),
onNavigateTo: (Destination) -> Unit = {}
) {
val context = LocalContext.current
val networkStatus by mainViewModel.networkProvider.status.collectAsStateWithLifecycle(
initialValue = NetworkStatus.AVAILABLE
)
val updates by mainViewModel.updateHelper.updates.collectAsStateWithLifecycle(
initialValue = null
)
val updateCount = updates?.size ?: 0
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState(
initialPage = initialTab.coerceIn(
0,
MainTab.entries.size - 1
)
) {
MainTab.entries.size
}
var showMoreSheet by remember { mutableStateOf(false) }
var appMenuTarget by remember { mutableStateOf<MinimalApp?>(null) }
var showNetworkDialog by remember { mutableStateOf(false) }
fun handleNavigation(destination: Destination) {
when (destination) {
is Destination.AppMenu -> appMenuTarget = destination.app
else -> onNavigateTo(destination)
}
}
LaunchedEffect(networkStatus) {
showNetworkDialog = networkStatus == NetworkStatus.UNAVAILABLE
}
if (showNetworkDialog) {
NetworkSheet()
}
if (showMoreSheet) {
MoreSheet(
onDismiss = { showMoreSheet = false },
onNavigateTo = { destination ->
showMoreSheet = false
onNavigateTo(destination)
}
)
}
appMenuTarget?.let { app ->
AppMenuSheet(app = app, onDismiss = { appMenuTarget = null })
}
Scaffold(
topBar = {
TopAppBar(
title = stringResource(MainTab.entries[pagerState.currentPage].labelRes),
actions = {
IconButton(onClick = { onNavigateTo(Destination.Downloads) }) {
Icon(
painter = painterResource(R.drawable.ic_download_manager),
contentDescription = stringResource(R.string.title_download_manager)
)
}
IconButton(onClick = { showMoreSheet = true }) {
Icon(
painter = painterResource(R.drawable.ic_settings_account),
contentDescription = stringResource(R.string.title_more)
)
}
}
)
},
floatingActionButton = {
FloatingActionButton(onClick = { onNavigateTo(Destination.Search) }) {
Icon(
painter = painterResource(R.drawable.ic_round_search),
contentDescription = stringResource(R.string.action_search)
)
}
},
bottomBar = {
NavigationBar {
MainTab.entries.forEachIndexed { index, tab ->
NavigationBarItem(
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch { pagerState.animateScrollToPage(index) }
},
icon = {
if (tab == MainTab.UPDATES && updateCount > 0) {
BadgedBox(badge = { Badge { Text("$updateCount") } }) {
Icon(
painter = painterResource(tab.iconRes),
contentDescription = null
)
}
} else {
Icon(
painter = painterResource(tab.iconRes),
contentDescription = null
)
}
},
label = { Text(stringResource(tab.labelRes)) }
)
}
}
}
) { paddingValues ->
Box(
modifier = Modifier
.padding(paddingValues)
.consumeWindowInsets(paddingValues)
.fillMaxSize()
) {
HorizontalPager(
state = pagerState,
userScrollEnabled = false,
beyondViewportPageCount = MainTab.entries.size - 1,
modifier = Modifier.fillMaxSize()
) { page ->
when (MainTab.entries[page]) {
MainTab.APPS -> AppsGamesScreen(
pageType = 0,
onNavigateTo = ::handleNavigation
)
MainTab.GAMES -> AppsGamesScreen(
pageType = 1,
onNavigateTo = ::handleNavigation
)
MainTab.UPDATES -> UpdatesScreen(
viewModel = updatesViewModel,
onNavigateTo = ::handleNavigation,
onRequestUpdate = { update ->
if (update.fileList.requiresObbDir() &&
!isGranted(context, PermissionType.STORAGE_MANAGER)
) {
onNavigateTo(
Destination.PermissionRationale(
setOf(PermissionType.STORAGE_MANAGER)
)
)
} else {
updatesViewModel.download(update)
}
},
onRequestUpdateAll = {
val needsObb = updatesViewModel.updates.value?.any {
it.fileList.requiresObbDir()
} == true
if (needsObb && !isGranted(context, PermissionType.STORAGE_MANAGER)) {
onNavigateTo(
Destination.PermissionRationale(
setOf(PermissionType.STORAGE_MANAGER)
)
)
} else {
updatesViewModel.downloadAll()
}
},
onCancelUpdate = { packageName ->
updatesViewModel.cancelDownload(packageName)
},
onCancelAll = { updatesViewModel.cancelAll() }
)
}
}
}
}
}