diff --git a/app/src/main/java/com/aurora/store/compose/ui/apps/AppsGamesScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/apps/AppsGamesScreen.kt new file mode 100644 index 000000000..83451016c --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/apps/AppsGamesScreen.kt @@ -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)) } + ) + } + } + } +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/apps/CategoriesPage.kt b/app/src/main/java/com/aurora/store/compose/ui/apps/CategoriesPage.kt new file mode 100644 index 000000000..c07ae64a5 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/apps/CategoriesPage.kt @@ -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?, 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) +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/apps/ForYouPage.kt b/app/src/main/java/com/aurora/store/compose/ui/apps/ForYouPage.kt new file mode 100644 index 000000000..785580aed --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/apps/ForYouPage.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/apps/TopChartsPage.kt b/app/src/main/java/com/aurora/store/compose/ui/apps/TopChartsPage.kt new file mode 100644 index 000000000..347b03fc0 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/apps/TopChartsPage.kt @@ -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() + 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, + 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 + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/main/MainScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/main/MainScreen.kt new file mode 100644 index 000000000..0ea77714c --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/main/MainScreen.kt @@ -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(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() } + ) + } + } + } + } +}