From d12cd77e179e0eb49604fca3f9419d52bb0a5973 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Tue, 19 May 2026 12:09:33 +0530 Subject: [PATCH 01/27] compose: add shared composable foundations EmptyState, SectionHeader, Shimmer placeholders; TopAppBar and ScrollHint refinements to support the migrated screens. --- .../store/compose/composable/EmptyState.kt | 77 +++++ .../store/compose/composable/ScrollHint.kt | 5 +- .../store/compose/composable/SectionHeader.kt | 116 ++++++++ .../store/compose/composable/Shimmer.kt | 274 ++++++++++++++++++ .../store/compose/composable/TopAppBar.kt | 18 +- 5 files changed, 481 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/aurora/store/compose/composable/EmptyState.kt create mode 100644 app/src/main/java/com/aurora/store/compose/composable/SectionHeader.kt create mode 100644 app/src/main/java/com/aurora/store/compose/composable/Shimmer.kt diff --git a/app/src/main/java/com/aurora/store/compose/composable/EmptyState.kt b/app/src/main/java/com/aurora/store/compose/composable/EmptyState.kt new file mode 100644 index 000000000..8e973ccbd --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composable/EmptyState.kt @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composable + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import com.aurora.store.R +import com.aurora.store.compose.preview.ThemePreviewProvider + +@Composable +fun EmptyState( + modifier: Modifier = Modifier, + @DrawableRes icon: Int, + @StringRes message: Int, + @StringRes actionLabel: Int? = null, + onAction: () -> Unit = {} +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy( + dimensionResource(R.dimen.margin_small), + Alignment.CenterVertically + ) + ) { + Icon( + painter = painterResource(icon), + contentDescription = null, + modifier = Modifier.size(dimensionResource(R.dimen.icon_size_medium)), + tint = MaterialTheme.colorScheme.primary + ) + + Text( + text = stringResource(message), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onPrimary + ) + + if (actionLabel != null) { + FilledTonalButton(onClick = onAction) { + Text(stringResource(actionLabel)) + } + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun EmptyStatePreview() { + EmptyState( + icon = R.drawable.ic_updates, + message = R.string.details_no_updates, + actionLabel = R.string.check_updates + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composable/ScrollHint.kt b/app/src/main/java/com/aurora/store/compose/composable/ScrollHint.kt index 3597f508d..f73d9a191 100644 --- a/app/src/main/java/com/aurora/store/compose/composable/ScrollHint.kt +++ b/app/src/main/java/com/aurora/store/compose/composable/ScrollHint.kt @@ -36,8 +36,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.aurora.store.R @@ -48,7 +48,6 @@ import kotlinx.coroutines.launch fun ScrollHint( listState: LazyListState, modifier: Modifier = Modifier, - bottomPadding: Dp = 72.dp, // keep above bottom bar autoHideOnScroll: Boolean = true, enableBounce: Boolean = true, onClickScrollOffset: Float = 300f @@ -88,7 +87,7 @@ fun ScrollHint( visible = visible, modifier = modifier .fillMaxWidth() - .padding(bottom = bottomPadding), + .padding(bottom = dimensionResource(R.dimen.scroll_hint_padding)), enter = fadeIn() + slideInVertically { it / 2 }, exit = fadeOut() ) { diff --git a/app/src/main/java/com/aurora/store/compose/composable/SectionHeader.kt b/app/src/main/java/com/aurora/store/compose/composable/SectionHeader.kt new file mode 100644 index 000000000..960aaf6d8 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composable/SectionHeader.kt @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composable + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import com.aurora.store.R +import com.aurora.store.compose.preview.ThemePreviewProvider + +/** + * Section header row with optional right-chevron for navigation (replaces HeaderView). + * When [browseUrl] is non-null and non-blank the row is clickable and shows a chevron. + */ +@Composable +fun SectionHeader( + modifier: Modifier = Modifier, + title: String, + browseUrl: String? = null, + onClick: () -> Unit = {} +) { + val hasLink = !browseUrl.isNullOrBlank() + Row( + modifier = modifier + .fillMaxWidth() + .then(if (hasLink) Modifier.clickable(onClick = onClick) else Modifier) + .padding(all = dimensionResource(R.dimen.padding_medium)), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + if (hasLink) { + Spacer(Modifier.width(dimensionResource(R.dimen.margin_small))) + Icon( + painter = painterResource(R.drawable.ic_arrow_right), + contentDescription = null, + modifier = Modifier.size(dimensionResource(R.dimen.icon_size_default)), + tint = MaterialTheme.colorScheme.primary + ) + } + } +} + +/** + * Section header row with a right-aligned action button (replaces UpdateHeaderView). + */ +@Composable +fun SectionHeaderWithAction( + modifier: Modifier = Modifier, + title: String, + action: String, + onAction: () -> Unit = {} +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding( + start = dimensionResource(R.dimen.padding_small), + top = dimensionResource(R.dimen.padding_xxsmall), + bottom = dimensionResource(R.dimen.padding_xxsmall) + ), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + TextButton(onClick = onAction) { + Text(action) + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview(showBackground = true) +@Composable +private fun SectionHeaderPreview() { + SectionHeader(title = "Top Charts", browseUrl = "https://example.com") +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview(showBackground = true) +@Composable +private fun SectionHeaderWithActionPreview() { + SectionHeaderWithAction(title = "3 updates available", action = "Update all") +} diff --git a/app/src/main/java/com/aurora/store/compose/composable/Shimmer.kt b/app/src/main/java/com/aurora/store/compose/composable/Shimmer.kt new file mode 100644 index 000000000..5e663fa0f --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composable/Shimmer.kt @@ -0,0 +1,274 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composable + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.compose.ui.unit.dp +import com.aurora.store.R +import com.aurora.store.compose.preview.ThemePreviewProvider + +@Composable +fun shimmerBrush(): Brush { + val base = MaterialTheme.colorScheme.onSurface + val colors = listOf( + base.copy(alpha = 0.08f), + base.copy(alpha = 0.20f), + base.copy(alpha = 0.08f) + ) + val transition = rememberInfiniteTransition(label = "shimmer") + val offset by transition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(1000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ), + label = "shimmer_offset" + ) + val x = offset * 1200f + return Brush.linearGradient( + colors = colors, + start = Offset(x - 400f, 0f), + end = Offset(x, 400f) + ) +} + +@Composable +internal fun ShimmerCategoryRow() { + val brush = shimmerBrush() + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = dimensionResource(R.dimen.padding_medium), + vertical = dimensionResource(R.dimen.padding_small) + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_large)) + ) { + Box( + modifier = Modifier + .requiredSize(dimensionResource(R.dimen.icon_size_category)) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small))) + .background(brush) + ) + + Box( + modifier = Modifier + .weight(1f) + .height(24.dp) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small))) + .background(brush) + ) + } +} + +@Composable +internal fun ShimmerAppRow() { + val brush = shimmerBrush() + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = dimensionResource(R.dimen.padding_small), + vertical = dimensionResource(R.dimen.padding_xsmall) + ), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .requiredSize(dimensionResource(R.dimen.icon_size_medium)) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small))) + .background(brush) + ) + Column( + modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small)) + ) { + repeat(3) { i -> + Box( + modifier = Modifier + .fillMaxWidth(if (i == 0) 0.75f else 0.5f) + .height(14.dp) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small))) + .background(brush) + ) + if (i < 2) Spacer(Modifier.height(dimensionResource(R.dimen.padding_xxsmall))) + } + } + } +} + +@Composable +internal fun ShimmerUpdateItem() { + val brush = shimmerBrush() + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = dimensionResource(R.dimen.padding_small), + vertical = dimensionResource(R.dimen.padding_xsmall) + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_small)) + ) { + Box( + modifier = Modifier + .requiredSize(dimensionResource(R.dimen.icon_size_medium)) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small))) + .background(brush) + ) + + Column(modifier = Modifier.weight(1f)) { + repeat(3) { i -> + Box( + modifier = Modifier + .fillMaxWidth(if (i == 0) 0.7f else 0.5f) + .height(14.dp) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small))) + .background(brush) + ) + if (i < 2) Spacer(Modifier.height(dimensionResource(R.dimen.padding_xxsmall))) + } + } + + Box( + modifier = Modifier + .width(80.dp) + .height(36.dp) + .clip(RoundedCornerShape(50)) + .background(brush) + ) + } +} + +@Composable +internal fun ShimmerCarouselSection() { + val brush = shimmerBrush() + Column( + modifier = Modifier + .fillMaxWidth() + .padding(all = dimensionResource(R.dimen.padding_small)), + verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_small)) + ) { + Box( + modifier = Modifier + .fillMaxWidth(0.42f) + .height(16.dp) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small))) + .background(brush) + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_small)) + ) { + repeat(5) { + Column( + verticalArrangement = Arrangement.spacedBy( + dimensionResource(R.dimen.padding_xsmall) + ) + ) { + Box( + modifier = Modifier + .size(dimensionResource(R.dimen.icon_size_cluster)) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium))) + .background(brush) + ) + Box( + modifier = Modifier + .width(dimensionResource(R.dimen.icon_size_cluster) * 0.75f) + .height(11.dp) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small))) + .background(brush) + ) + Box( + modifier = Modifier + .width(dimensionResource(R.dimen.icon_size_cluster) * 0.5f) + .height(11.dp) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small))) + .background(brush) + ) + } + } + } + } +} + +@Composable +internal fun ShimmerAppListItem() { + val brush = shimmerBrush() + Box( + modifier = Modifier + .padding(dimensionResource(R.dimen.padding_xsmall)) + .size(dimensionResource(R.dimen.icon_size_cluster)) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium))) + .background(brush) + ) +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview(showBackground = true) +@Composable +private fun ShimmerCategoryRowPreview() { + ShimmerCategoryRow() +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview(showBackground = true) +@Composable +private fun ShimmerAppRowPreview() { + ShimmerAppRow() +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview(showBackground = true) +@Composable +private fun ShimmerUpdateItemPreview() { + ShimmerUpdateItem() +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview(showBackground = true) +@Composable +private fun ShimmerCarouselSectionPreview() { + ShimmerCarouselSection() +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview(showBackground = true) +@Composable +private fun ShimmerAppListItemPreview() { + ShimmerAppListItem() +} diff --git a/app/src/main/java/com/aurora/store/compose/composable/TopAppBar.kt b/app/src/main/java/com/aurora/store/compose/composable/TopAppBar.kt index c9043064f..3f9ab4713 100644 --- a/app/src/main/java/com/aurora/store/compose/composable/TopAppBar.kt +++ b/app/src/main/java/com/aurora/store/compose/composable/TopAppBar.kt @@ -5,11 +5,15 @@ package com.aurora.store.compose.composable +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter @@ -25,7 +29,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider * @param modifier The modifier to be applied to the composable * @param title Title of the screen * @param navigationIcon Icon for the navigation button - * @param onNavigateUp Action when user clicks the navigation icon + * @param showNavigationIcon Whether to show the navigation (back) icon button * @param actions Actions to display on the top app bar (for e.g. menu) */ @Composable @@ -33,15 +37,17 @@ fun TopAppBar( modifier: Modifier = Modifier, title: String? = null, navigationIcon: Painter = painterResource(R.drawable.ic_arrow_back), - onNavigateUp: (() -> Unit)? = null, + showNavigationIcon: Boolean = true, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, actions: @Composable (RowScope.() -> Unit) = {} ) { + val activity = LocalActivity.current as? ComponentActivity TopAppBar( modifier = modifier, title = { if (title != null) Text(text = title) }, navigationIcon = { - if (onNavigateUp != null) { - IconButton(onClick = onNavigateUp) { + if (showNavigationIcon) { + IconButton(onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }) { Icon( painter = navigationIcon, contentDescription = stringResource(R.string.action_back) @@ -49,6 +55,7 @@ fun TopAppBar( } } }, + windowInsets = windowInsets, actions = actions ) } @@ -58,7 +65,6 @@ fun TopAppBar( @Composable private fun TopAppBarPreview() { TopAppBar( - title = stringResource(R.string.title_about), - onNavigateUp = {} + title = stringResource(R.string.title_about) ) } From 21377100123c69410fe15fd7a5ff5bba61f29d28 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Tue, 19 May 2026 12:09:51 +0530 Subject: [PATCH 02/27] compose: add stream and app item composables StreamCarousel for cluster-based home/category lists, CategoryItem grid tile, and AppUpdateItem for the updates list. AnimatedAppIcon plus the existing AppListItem and LargeAppListItem are adjusted to fit the new call sites. --- .../store/compose/composable/CategoryItem.kt | 78 +++++++ .../compose/composable/StreamCarousel.kt | 187 ++++++++++++++++ .../compose/composable/app/AnimatedAppIcon.kt | 4 +- .../compose/composable/app/AppListItem.kt | 57 +++-- .../compose/composable/app/AppUpdateItem.kt | 209 ++++++++++++++++++ .../composable/app/LargeAppListItem.kt | 17 +- 6 files changed, 521 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/com/aurora/store/compose/composable/CategoryItem.kt create mode 100644 app/src/main/java/com/aurora/store/compose/composable/StreamCarousel.kt create mode 100644 app/src/main/java/com/aurora/store/compose/composable/app/AppUpdateItem.kt diff --git a/app/src/main/java/com/aurora/store/compose/composable/CategoryItem.kt b/app/src/main/java/com/aurora/store/compose/composable/CategoryItem.kt new file mode 100644 index 000000000..d891f33d0 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composable/CategoryItem.kt @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composable + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.request.crossfade +import com.aurora.gplayapi.data.models.Category +import com.aurora.store.R +import com.aurora.store.compose.preview.ThemePreviewProvider + +@Composable +fun CategoryItem(modifier: Modifier = Modifier, category: Category, onClick: () -> Unit = {}) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding( + horizontal = dimensionResource(R.dimen.padding_medium), + vertical = dimensionResource(R.dimen.padding_small) + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_large)) + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(category.imageUrl) + .crossfade(true) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .requiredSize(dimensionResource(R.dimen.icon_size_category)) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small))) + ) + + Text( + text = category.title, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview(showBackground = true) +@Composable +private fun CategoryItemPreview() { + CategoryItem( + category = Category( + title = "Entertainment", + imageUrl = "" + ) + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composable/StreamCarousel.kt b/app/src/main/java/com/aurora/store/compose/composable/StreamCarousel.kt new file mode 100644 index 000000000..1baf71c90 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composable/StreamCarousel.kt @@ -0,0 +1,187 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +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.StreamBundle +import com.aurora.gplayapi.data.models.StreamCluster +import com.aurora.store.R +import com.aurora.store.compose.composable.app.AppListItem +import com.aurora.store.compose.composable.app.LargeAppListItem +import com.aurora.store.compose.preview.ThemePreviewProvider +import kotlinx.coroutines.flow.distinctUntilChanged + +private const val LOAD_MORE_THRESHOLD = 2 + +@Composable +fun StreamCarousel( + modifier: Modifier = Modifier, + streamBundle: StreamBundle?, + filterSingleAppClusters: Boolean = true, + lazyListState: LazyListState = rememberLazyListState(), + onHeaderClick: (StreamCluster) -> Unit = {}, + onAppClick: (App) -> Unit = {}, + onAppLongClick: (App) -> Unit = {}, + onClusterScrolled: (StreamCluster) -> Unit = {}, + onScrolledToEnd: () -> Unit = {} +) { + val bundleLoaded = streamBundle != null + LaunchedEffect(lazyListState, bundleLoaded) { + snapshotFlow { + val last = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1 + val total = lazyListState.layoutInfo.totalItemsCount + last >= total - LOAD_MORE_THRESHOLD + }.distinctUntilChanged().collect { reachedEnd -> + if (reachedEnd && bundleLoaded) onScrolledToEnd() + } + } + + if (streamBundle == null) { + LazyColumn( + modifier = modifier.fillMaxSize(), + state = lazyListState, + verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium)) + ) { + items(5) { ShimmerCarouselSection() } + } + return + } + + val clusters = streamBundle.streamClusters.values + .filter { cluster -> + cluster.clusterAppList.isNotEmpty() && + cluster.clusterTitle.isNotBlank() && + (!filterSingleAppClusters || cluster.clusterAppList.size > 1) + } + + if (clusters.isEmpty()) { + EmptyState( + modifier = modifier, + icon = R.drawable.ic_apps, + message = R.string.no_apps_available + ) + return + } + + LazyColumn( + modifier = modifier.fillMaxSize(), + state = lazyListState, + verticalArrangement = Arrangement.spacedBy( + dimensionResource( + if (clusters.size == 1) { + R.dimen.margin_medium + } else { + R.dimen.margin_xxsmall + } + ) + ) + ) { + if (clusters.size == 1) { + val apps = clusters.first().clusterAppList + items(count = apps.size, key = { apps[it].id }) { index -> + LargeAppListItem( + app = apps[index], + onClick = { onAppClick(apps[index]) } + ) + } + } else { + clusters.forEach { cluster -> + item(key = "header_${cluster.id}") { + SectionHeader( + title = cluster.clusterTitle, + browseUrl = cluster.clusterBrowseUrl, + onClick = { onHeaderClick(cluster) } + ) + } + item(key = "row_${cluster.id}") { + ClusterRow( + cluster = cluster, + onAppClick = onAppClick, + onAppLongClick = onAppLongClick, + onClusterScrolled = onClusterScrolled + ) + } + } + } + + if (streamBundle.hasNext()) { + item(key = "shimmer_footer") { ShimmerCarouselSection() } + } + } +} + +@Composable +private fun ClusterRow( + cluster: StreamCluster, + onAppClick: (App) -> Unit, + onAppLongClick: (App) -> Unit, + onClusterScrolled: (StreamCluster) -> Unit +) { + val rowState = rememberLazyListState() + val reachedEnd by remember { + derivedStateOf { + val last = rowState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1 + val total = rowState.layoutInfo.totalItemsCount + last >= total - LOAD_MORE_THRESHOLD + } + } + + LaunchedEffect(reachedEnd) { + if (reachedEnd && cluster.hasNext()) onClusterScrolled(cluster) + } + + LazyRow( + state = rowState, + contentPadding = PaddingValues(horizontal = dimensionResource(R.dimen.padding_small)), + horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_small)) + ) { + itemsIndexed( + items = cluster.clusterAppList, + key = { _, app -> app.packageName } + ) { _, app -> + AppListItem( + app = app, + onClick = { onAppClick(app) }, + onLongClick = { onAppLongClick(app) } + ) + } + if (cluster.hasNext()) { + item(key = "shimmer_${cluster.id}") { ShimmerAppListItem() } + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview(showBackground = true) +@Composable +private fun StreamCarouselLoadingPreview() { + StreamCarousel(streamBundle = null) +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview(showBackground = true) +@Composable +private fun StreamCarouselEmptyPreview() { + StreamCarousel(streamBundle = StreamBundle.EMPTY) +} diff --git a/app/src/main/java/com/aurora/store/compose/composable/app/AnimatedAppIcon.kt b/app/src/main/java/com/aurora/store/compose/composable/app/AnimatedAppIcon.kt index 860988813..f75de3896 100644 --- a/app/src/main/java/com/aurora/store/compose/composable/app/AnimatedAppIcon.kt +++ b/app/src/main/java/com/aurora/store/compose/composable/app/AnimatedAppIcon.kt @@ -111,7 +111,9 @@ private fun AnimatedAppIconPreview(@PreviewParameter(AppPreviewProvider::class) @PreviewWrapper(ThemePreviewProvider::class) @Preview(showBackground = true) @Composable -private fun AnimatedAppIconPreview(@PreviewParameter(ProgressProvider::class) progress: Float) { +private fun AnimatedAppIconInProgressPreview( + @PreviewParameter(ProgressProvider::class) progress: Float +) { AnimatedAppIcon( modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)), iconUrl = "", diff --git a/app/src/main/java/com/aurora/store/compose/composable/app/AppListItem.kt b/app/src/main/java/com/aurora/store/compose/composable/app/AppListItem.kt index fed710407..73f4057d9 100644 --- a/app/src/main/java/com/aurora/store/compose/composable/app/AppListItem.kt +++ b/app/src/main/java/com/aurora/store/compose/composable/app/AppListItem.kt @@ -5,21 +5,25 @@ package com.aurora.store.compose.composable.app -import android.text.format.Formatter -import androidx.compose.foundation.clickable +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -37,42 +41,49 @@ import com.aurora.store.compose.preview.ThemePreviewProvider * @param modifier The modifier to be applied to the composable * @param app [App] to display * @param onClick Callback when the composable is clicked + * @param onLongClick Callback when the composable is long-clicked * @see LargeAppListItem */ +@OptIn(ExperimentalFoundationApi::class) @Composable -fun AppListItem(modifier: Modifier = Modifier, app: App, onClick: () -> Unit = {}) { - val context = LocalContext.current - +fun AppListItem( + modifier: Modifier = Modifier, + app: App, + onClick: () -> Unit = {}, + onLongClick: () -> Unit = {} +) { Column( modifier = modifier .width(dimensionResource(R.dimen.icon_size_cluster)) - .clickable(onClick = onClick) - .padding(dimensionResource(R.dimen.padding_xsmall)) + .combinedClickable(onClick = onClick, onLongClick = onLongClick) + .padding(all = dimensionResource(R.dimen.padding_xxsmall)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy( + dimensionResource(R.dimen.margin_xsmall), + Alignment.CenterVertically + ) ) { AsyncImage( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium))), model = ImageRequest.Builder(LocalContext.current) .data(app.iconArtwork.url) .crossfade(true) .build(), contentDescription = null, - contentScale = ContentScale.Crop, + contentScale = ContentScale.Crop + + ) + Text( modifier = Modifier - .requiredSize(dimensionResource(R.dimen.icon_size_cluster)) - .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium))) - ) - Text( + .fillMaxWidth(), text = app.displayName, - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.labelMedium, maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - Text( - text = if (app.size > 0) { - Formatter.formatShortFileSize(context, app.size) - } else { - app.downloadString - }, - style = MaterialTheme.typography.bodySmall + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Start ) } } diff --git a/app/src/main/java/com/aurora/store/compose/composable/app/AppUpdateItem.kt b/app/src/main/java/com/aurora/store/compose/composable/app/AppUpdateItem.kt new file mode 100644 index 000000000..d1dfd06c5 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composable/app/AppUpdateItem.kt @@ -0,0 +1,209 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composable.app + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +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.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import com.aurora.store.R +import com.aurora.store.compose.preview.ThemePreviewProvider +import com.aurora.store.data.model.DownloadStatus +import com.aurora.store.data.room.download.Download +import com.aurora.store.data.room.update.Update +import com.aurora.store.util.CommonUtil + +@Composable +fun AppUpdateItem( + modifier: Modifier = Modifier, + update: Update, + download: Download? = null, + onClick: () -> Unit = {}, + onLongClick: () -> Unit = {}, + onUpdate: () -> Unit = {}, + onCancel: () -> Unit = {} +) { + val inProgress = download != null && download.status !in DownloadStatus.finished + val progress = if (download?.status == DownloadStatus.DOWNLOADING) { + download.progress.toFloat() + } else { + 0f + } + var changelogExpanded by remember { mutableStateOf(false) } + + Column(modifier = modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .combinedClickable(onClick = onClick, onLongClick = onLongClick) + .padding( + horizontal = dimensionResource(R.dimen.padding_medium), + vertical = dimensionResource(R.dimen.padding_small) + ), + verticalAlignment = Alignment.CenterVertically + ) { + Box(modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium))) { + AnimatedAppIcon( + modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)), + iconUrl = update.iconURL, + inProgress = inProgress, + progress = progress + ) + } + Spacer(Modifier.width(dimensionResource(R.dimen.margin_small))) + Column(modifier = Modifier.weight(1f)) { + Text( + text = update.displayName, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = update.developerName, + style = MaterialTheme.typography.bodySmall + ) + Text( + text = "${CommonUtil.addSiPrefix(update.size)} • ${update.updatedOn}", + style = MaterialTheme.typography.bodySmall + ) + Text( + text = "${update.versionName} (${update.versionCode})", + style = MaterialTheme.typography.bodySmall + ) + } + Spacer(Modifier.width(dimensionResource(R.dimen.margin_small))) + if (inProgress) { + OutlinedButton(onClick = onCancel) { + Text(stringResource(R.string.action_cancel)) + } + } else { + Button(onClick = onUpdate) { + Text(stringResource(R.string.action_update)) + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { changelogExpanded = !changelogExpanded } + .padding(horizontal = dimensionResource(R.dimen.padding_medium)), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(Modifier.weight(1f)) + IconButton(onClick = { changelogExpanded = !changelogExpanded }) { + Icon( + painter = painterResource( + if (changelogExpanded) R.drawable.ic_arrow_up else R.drawable.ic_arrow_down + ), + contentDescription = stringResource(R.string.expand) + ) + } + } + + AnimatedVisibility(visible = changelogExpanded) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = dimensionResource(R.dimen.padding_medium), + vertical = dimensionResource(R.dimen.padding_small) + ), + shape = RoundedCornerShape(dimensionResource(R.dimen.radius_small)) + ) { + Text( + text = if (update.changelog.isNotEmpty()) { + AnnotatedString.fromHtml(update.changelog) + } else { + AnnotatedString(stringResource(R.string.details_changelog_unavailable)) + }, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium)) + ) + } + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview(showBackground = true) +@Composable +private fun AppUpdateItemPreview() { + val update = Update( + packageName = "com.example.app", + versionCode = 100, + versionName = "1.0.0", + displayName = "Example App", + iconURL = "", + changelog = "• Bug fixes
• Performance improvements", + id = 1, + developerName = "Developer", + size = 5_000_000, + updatedOn = "May 2025", + hasValidCert = true, + offerType = 1, + fileList = emptyList(), + sharedLibs = emptyList() + ) + AppUpdateItem(update = update) +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview(showBackground = true) +@Composable +private fun AppUpdateItemDownloadingPreview() { + val update = Update( + packageName = "com.example.app", + versionCode = 100, + versionName = "1.0.0", + displayName = "Example App", + iconURL = "", + changelog = "", + id = 1, + developerName = "Developer", + size = 5_000_000, + updatedOn = "May 2025", + hasValidCert = true, + offerType = 1, + fileList = emptyList(), + sharedLibs = emptyList() + ) + val download = Download.fromUpdate(update).copy( + status = DownloadStatus.DOWNLOADING, + progress = 45 + ) + AppUpdateItem(update = update, download = download) +} diff --git a/app/src/main/java/com/aurora/store/compose/composable/app/LargeAppListItem.kt b/app/src/main/java/com/aurora/store/compose/composable/app/LargeAppListItem.kt index fbb949c8d..bc78fdc63 100644 --- a/app/src/main/java/com/aurora/store/compose/composable/app/LargeAppListItem.kt +++ b/app/src/main/java/com/aurora/store/compose/composable/app/LargeAppListItem.kt @@ -71,23 +71,26 @@ fun LargeAppListItem(modifier: Modifier = Modifier, app: App, onClick: () -> Uni .fillMaxWidth() .clickable(onClick = onClick) .padding( - horizontal = dimensionResource(R.dimen.padding_medium), - vertical = dimensionResource(R.dimen.padding_small) + horizontal = dimensionResource(R.dimen.padding_small), + vertical = dimensionResource(R.dimen.padding_xsmall) ) ) { AsyncImage( + modifier = Modifier + .requiredSize(dimensionResource(R.dimen.icon_size_medium)) + .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium))), model = ImageRequest.Builder(LocalContext.current) .data(app.iconArtwork.url) .crossfade(true) .build(), contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .requiredSize(dimensionResource(R.dimen.icon_size_medium)) - .clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium))) + contentScale = ContentScale.Crop ) + Column( - modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small)) + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensionResource(R.dimen.margin_small)) ) { Text( text = app.displayName, From 7617a9b953c1172a00214d614027ffc6520dd3fd Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Tue, 19 May 2026 12:10:06 +0530 Subject: [PATCH 03/27] compose: add StateFlow/LiveData collectForced helpers Bridges Flows whose values have broken equals() (the gplayapi StreamBundle/StreamCluster id-only-equals quirk) into Compose state by forcing recomposition via neverEqualPolicy. --- .../compose/composition/FlowExtensions.kt | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 app/src/main/java/com/aurora/store/compose/composition/FlowExtensions.kt diff --git a/app/src/main/java/com/aurora/store/compose/composition/FlowExtensions.kt b/app/src/main/java/com/aurora/store/compose/composition/FlowExtensions.kt new file mode 100644 index 000000000..e14f8a653 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composition/FlowExtensions.kt @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composition + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.neverEqualPolicy +import androidx.compose.runtime.remember +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +/** + * Collects a [StateFlow] as [State], always triggering recomposition on every emission + * regardless of structural equality, by using [neverEqualPolicy]. + */ +@Composable +internal fun StateFlow.collectForced(): State { + val lifecycleOwner = LocalLifecycleOwner.current + val state: MutableState = remember(this) { mutableStateOf(value, neverEqualPolicy()) } + LaunchedEffect(this, lifecycleOwner) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + collect { state.value = it } + } + } + return state +} + +/** + * Collects a [Flow] as [State] starting from [initial], always triggering recomposition on + * every emission regardless of structural equality, by using [neverEqualPolicy]. + * + * Useful for non-state flows (e.g. [kotlinx.coroutines.flow.SharedFlow]) and for flows whose + * values have broken `equals` implementations that would otherwise be conflated upstream. + */ +@Composable +internal fun Flow.collectForced(initial: T): State { + val lifecycleOwner = LocalLifecycleOwner.current + val state: MutableState = remember(this) { mutableStateOf(initial, neverEqualPolicy()) } + LaunchedEffect(this, lifecycleOwner) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + collect { state.value = it } + } + } + return state +} + +/** + * Observes a [LiveData] as [State], always triggering recomposition on every emission + * regardless of structural equality, by using [neverEqualPolicy]. + */ +@Suppress("UNCHECKED_CAST") +@Composable +internal fun LiveData.observeForced(): State { + val lifecycleOwner = LocalLifecycleOwner.current + val state: MutableState = remember { mutableStateOf(value, neverEqualPolicy()) } + DisposableEffect(this, lifecycleOwner) { + val observer = Observer { state.value = it } + observe(lifecycleOwner, observer) + onDispose { removeObserver(observer) } + } + return state +} From baf593bd51ab585cbf4e554bc30a170dfb97c9cf Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Tue, 19 May 2026 12:10:59 +0530 Subject: [PATCH 04/27] compose: rewire navigation as the single entry point ComposeActivity becomes the sole launch target; the legacy MainActivity is removed. Destination / Screen pair drives navigation3, NavDisplay handles deep links plus microG-aware resume rechecks, and Theme.kt is tightened for edge-to-edge. --- app/src/main/AndroidManifest.xml | 30 ++- .../java/com/aurora/store/ComposeActivity.kt | 46 +++- .../java/com/aurora/store/MainActivity.kt | 157 ----------- .../store/compose/navigation/Destination.kt | 48 ++++ .../store/compose/navigation/NavDisplay.kt | 246 +++++++++++------- .../aurora/store/compose/navigation/Screen.kt | 30 +++ .../com/aurora/store/compose/theme/Theme.kt | 22 +- 7 files changed, 301 insertions(+), 278 deletions(-) delete mode 100644 app/src/main/java/com/aurora/store/MainActivity.kt create mode 100644 app/src/main/java/com/aurora/store/compose/navigation/Destination.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e7d15f5ec..ee020d093 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -82,9 +82,9 @@ tools:targetApi="tiramisu"> + android:windowSoftInputMode="adjustResize"> @@ -103,16 +103,28 @@ + + - + + + + + + + + + + + + + + + + + - - - UI.TV @@ -57,4 +49,32 @@ class ComposeActivity : ComponentActivity() { } } } + + private fun resolveStartDestination(): Screen { + // Parcel-based navigation (e.g. from NotificationUtil) + IntentCompat.getParcelableExtra(intent, Screen.PARCEL_KEY, Screen::class.java) + ?.let { return it } + + // Deep links via ACTION_VIEW + if (intent.action == Intent.ACTION_VIEW) { + val data = intent.data + val path = data?.path.orEmpty() + val id = data?.getQueryParameter("id") + return when { + id != null && path.contains("/apps/dev") -> Screen.DevProfile(id) + id != null -> Screen.AppDetails(id) + else -> defaultStart() + } + } + + // SEND / SHOW_APP_INFO — getPackageName() handles both + intent.getPackageName()?.let { return Screen.AppDetails(it) } + + return defaultStart() + } + + private fun defaultStart(): Screen = when { + !Preferences.getBoolean(this, Preferences.PREFERENCE_INTRO) -> Screen.Onboarding + else -> Screen.Splash + } } diff --git a/app/src/main/java/com/aurora/store/MainActivity.kt b/app/src/main/java/com/aurora/store/MainActivity.kt deleted file mode 100644 index 683f22517..000000000 --- a/app/src/main/java/com/aurora/store/MainActivity.kt +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * Copyright (C) 2022, The Calyx Institute - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store - -import android.os.Bundle -import android.view.View -import androidx.activity.addCallback -import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat.Type.displayCutout -import androidx.core.view.WindowInsetsCompat.Type.ime -import androidx.core.view.WindowInsetsCompat.Type.systemBars -import androidx.lifecycle.lifecycleScope -import androidx.navigation.FloatingWindow -import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.setupWithNavController -import com.aurora.store.data.model.NetworkStatus -import com.aurora.store.data.receiver.MigrationReceiver -import com.aurora.store.databinding.ActivityMainBinding -import com.aurora.store.util.PackageUtil -import com.aurora.store.util.Preferences -import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB -import com.aurora.store.view.ui.sheets.NetworkDialogSheet -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch - -@AndroidEntryPoint -class MainActivity : AppCompatActivity() { - - private val viewModel: MainViewModel by viewModels() - - private lateinit var binding: ActivityMainBinding - - // TopLevelFragments - private val topLevelFrags = listOf( - R.id.appsContainerFragment, - R.id.gamesContainerFragment, - R.id.updatesFragment - ) - - override fun onCreate(savedInstanceState: Bundle?) { - // Check and run migrations first if required - // This is needed thanks to OEMs breaking the MY_PACKAGE_REPLACED API - MigrationReceiver.runMigrationsIfRequired(this) - - enableEdgeToEdge() - super.onCreate(savedInstanceState) - - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - - // Adjust root view's paddings for edgeToEdge display - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { root, windowInsets -> - val insets = windowInsets.getInsets(systemBars() or displayCutout() or ime()) - root.setPadding(insets.left, insets.top, insets.right, 0) - windowInsets - } - - val navHostFragment = - supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment - val navController = navHostFragment.navController - - if (!PackageUtil.isTv(this)) { - viewModel.networkProvider.status.onEach { networkStatus -> - when (networkStatus) { - NetworkStatus.AVAILABLE -> { - if (!supportFragmentManager.isDestroyed && isIntroDone()) { - val fragment = supportFragmentManager - .findFragmentByTag(NetworkDialogSheet.TAG) - fragment?.let { - supportFragmentManager.beginTransaction() - .remove(fragment) - .commitAllowingStateLoss() - } - } - } - - NetworkStatus.UNAVAILABLE -> { - if (!supportFragmentManager.isDestroyed && isIntroDone()) { - supportFragmentManager.beginTransaction() - .add(NetworkDialogSheet.newInstance(), NetworkDialogSheet.TAG) - .commitAllowingStateLoss() - } - } - } - }.launchIn(AuroraApp.scope) - } - - binding.navView.setupWithNavController(navController) - - // Handle quick exit from back actions - val defaultTab = when (Preferences.getInteger(this, PREFERENCE_DEFAULT_SELECTED_TAB)) { - 1 -> R.id.gamesContainerFragment - 2 -> R.id.updatesFragment - else -> R.id.appsContainerFragment - } - onBackPressedDispatcher.addCallback(this) { - if (navController.currentDestination?.id in topLevelFrags) { - if (navController.currentDestination?.id == defaultTab) { - finish() - } else { - navController.navigate(defaultTab) - } - } else if (navHostFragment.childFragmentManager.backStackEntryCount == 0) { - // We are on either on onboarding or splash fragment - finish() - } else { - navController.navigateUp() - } - } - - // Handle views on fragments - navController.addOnDestinationChangedListener { _, navDestination, _ -> - if (navDestination !is FloatingWindow) { - when (navDestination.id) { - in topLevelFrags -> binding.navView.visibility = View.VISIBLE - else -> binding.navView.visibility = View.GONE - } - } - } - - // Updates - lifecycleScope.launch { - viewModel.updateHelper.updates.collectLatest { list -> - binding.navView.getOrCreateBadge(R.id.updatesFragment).apply { - isVisible = !list.isNullOrEmpty() - number = list?.size ?: 0 - } - } - } - } - - private fun isIntroDone(): Boolean = Preferences.getBoolean(this, Preferences.PREFERENCE_INTRO) -} diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt b/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt new file mode 100644 index 000000000..633597f05 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.navigation + +import com.aurora.gplayapi.data.models.Category +import com.aurora.gplayapi.data.models.StreamCluster +import com.aurora.store.data.model.MinimalApp +import com.aurora.store.data.model.PermissionType + +/** + * All navigation actions available to composable screens. + * Screens emit one of these via a single `onNavigateTo: (Destination) -> Unit` callback. + */ +sealed class Destination { + data object Splash : Destination() + data class Main(val initialTab: Int) : Destination() + + data class AppDetails(val packageName: String) : Destination() + data class DevProfile(val devId: String) : Destination() + data class AppMenu(val app: MinimalApp) : Destination() + + data object Search : Destination() + data object Downloads : Destination() + + data class StreamBrowse(val cluster: StreamCluster) : Destination() + data class ExpandedStreamBrowse(val title: String, val browseUrl: String) : Destination() + data class CategoryBrowse(val category: Category) : Destination() + data class PermissionRationale(val permissions: Set) : Destination() + + data object Accounts : Destination() + data object GoogleLogin : Destination() + data object About : Destination() + data object Favourite : Destination() + data object Spoof : Destination() + data object Installed : Destination() + data object Blacklist : Destination() + + data object Settings : Destination() + data object InstallationPreference : Destination() + data object Installer : Destination() + data object NetworkPreference : Destination() + data object Dispenser : Destination() + data object UIPreference : Destination() + data object UpdatesPreference : Destination() +} diff --git a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt index 96ff65379..d379dbf4b 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt @@ -7,16 +7,23 @@ package com.aurora.store.compose.navigation import android.content.Intent -import androidx.activity.compose.LocalActivity +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator -import androidx.navigation.NavDeepLinkBuilder import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.metadata import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay @@ -24,11 +31,13 @@ import com.aurora.Constants.PACKAGE_NAME_GMS import com.aurora.extensions.toast import com.aurora.store.AuroraApp import com.aurora.store.ComposeActivity -import com.aurora.store.MainActivity import com.aurora.store.R import com.aurora.store.compose.ui.about.AboutScreen import com.aurora.store.compose.ui.accounts.AccountsScreen +import com.aurora.store.compose.ui.accounts.GoogleLoginScreen import com.aurora.store.compose.ui.blacklist.BlacklistScreen +import com.aurora.store.compose.ui.commons.CategoryBrowseScreen +import com.aurora.store.compose.ui.commons.ExpandedStreamBrowseScreen import com.aurora.store.compose.ui.commons.PermissionRationaleScreen import com.aurora.store.compose.ui.commons.StreamBrowseScreen import com.aurora.store.compose.ui.details.AppDetailsScreen @@ -37,9 +46,16 @@ import com.aurora.store.compose.ui.dispenser.DispenserScreen import com.aurora.store.compose.ui.downloads.DownloadsScreen import com.aurora.store.compose.ui.favourite.FavouriteScreen import com.aurora.store.compose.ui.installed.InstalledScreen +import com.aurora.store.compose.ui.main.MainScreen import com.aurora.store.compose.ui.onboarding.OnboardingScreen +import com.aurora.store.compose.ui.preferences.SettingsScreen +import com.aurora.store.compose.ui.preferences.UIPreferenceScreen +import com.aurora.store.compose.ui.preferences.installation.InstallationPreferenceScreen import com.aurora.store.compose.ui.preferences.installation.InstallerScreen +import com.aurora.store.compose.ui.preferences.network.NetworkPreferenceScreen +import com.aurora.store.compose.ui.preferences.updates.UpdatesPreferenceScreen import com.aurora.store.compose.ui.search.SearchScreen +import com.aurora.store.compose.ui.splash.SplashScreen import com.aurora.store.compose.ui.spoof.SpoofScreen import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.model.AccountType @@ -54,23 +70,6 @@ import com.aurora.store.util.Preferences @Composable fun NavDisplay(startDestination: NavKey) { val backstack = rememberNavBackStack(startDestination) - - // TODO: Rework when migrating splash fragment to compose - val splashIntent = NavDeepLinkBuilder(LocalContext.current) - .setGraph(R.navigation.mobile_navigation) - .setDestination(R.id.splashFragment) - .setComponentName(MainActivity::class.java) - .createTaskStackBuilder() - .intents - .first() - .apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) } - - // TODO: Drop this logic once everything is in compose - val activity = LocalActivity.current - fun onNavigateUp() { - if (backstack.size == 1) activity?.finish() else backstack.removeLastOrNull() - } - val context = LocalContext.current fun isMicroGAuthInvalidated(): Boolean = @@ -101,114 +100,169 @@ fun NavDisplay(startDestination: NavKey) { } } + fun navigate(destination: Destination) { + when (destination) { + Destination.Splash -> { + // Clear the backstack when navigating to Splash to prevent going back to the previous screen when the user is sent back to the splash screen (e.g. after logout). + backstack.clear() + backstack.add(Screen.Splash) + } + + is Destination.Main -> { + // Clear the backstack when navigating to Main to prevent going back to the splash screen or other screens. + backstack.clear() + backstack.add(Screen.Main(destination.initialTab)) + } + + is Destination.ExpandedStreamBrowse -> backstack.add( + Screen.ExpandedStreamBrowse(destination.title, destination.browseUrl) + ) + + is Destination.CategoryBrowse -> backstack.add( + Screen.CategoryBrowse(destination.category.title, destination.category.browseUrl) + ) + + is Destination.PermissionRationale -> backstack.add( + Screen.PermissionRationale(destination.permissions) + ) + + is Destination.AppDetails -> backstack.add(Screen.AppDetails(destination.packageName)) + is Destination.DevProfile -> backstack.add(Screen.DevProfile(destination.devId)) + is Destination.AppMenu -> Unit + is Destination.StreamBrowse -> backstack.add(Screen.StreamBrowse(destination.cluster)) + + Destination.Search -> backstack.add(Screen.Search) + Destination.Downloads -> backstack.add(Screen.Downloads) + Destination.Accounts -> backstack.add(Screen.Accounts) + Destination.GoogleLogin -> backstack.add(Screen.GoogleLogin) + Destination.About -> backstack.add(Screen.About) + Destination.Favourite -> backstack.add(Screen.Favourite) + Destination.Spoof -> backstack.add(Screen.Spoof) + Destination.Installed -> backstack.add(Screen.Installed) + Destination.Blacklist -> backstack.add(Screen.Blacklist) + Destination.Settings -> backstack.add(Screen.Settings) + Destination.InstallationPreference -> backstack.add(Screen.InstallationPreference) + Destination.Installer -> backstack.add(Screen.Installer) + Destination.NetworkPreference -> backstack.add(Screen.NetworkPreference) + Destination.Dispenser -> backstack.add(Screen.Dispenser) + Destination.UIPreference -> backstack.add(Screen.UIPreference) + Destination.UpdatesPreference -> backstack.add(Screen.UpdatesPreference) + } + } + NavDisplay( + onBack = { backstack.removeLastOrNull() }, backStack = backstack, entryDecorators = listOf( rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator() ), + transitionSpec = { + slideInHorizontally( + animationSpec = spring(dampingRatio = 0.8f, stiffness = 380f), + initialOffsetX = { it } + ) togetherWith slideOutHorizontally(targetOffsetX = { -it }) + }, + popTransitionSpec = { + slideInHorizontally( + animationSpec = spring(dampingRatio = 0.8f, stiffness = 380f), + initialOffsetX = { -it } + ) togetherWith slideOutHorizontally(targetOffsetX = { it }) + }, + predictivePopTransitionSpec = { + slideInHorizontally( + animationSpec = spring(dampingRatio = 0.8f, stiffness = 380f), + initialOffsetX = { -it } + ) togetherWith slideOutHorizontally(targetOffsetX = { it }) + }, entryProvider = entryProvider { - entry { - BlacklistScreen(onNavigateUp = ::onNavigateUp) - } - - entry { - SearchScreen(onNavigateUp = ::onNavigateUp) + entry { screen -> + MainScreen( + initialTab = screen.initialTab, + onNavigateTo = ::navigate + ) } entry { screen -> AppDetailsScreen( packageName = screen.packageName, - onNavigateUp = ::onNavigateUp, - onNavigateToAppDetails = { packageName -> - backstack.add(Screen.AppDetails(packageName)) - } + onNavigateTo = ::navigate ) } entry { screen -> DevProfileScreen( developerId = screen.developerId, - onNavigateUp = ::onNavigateUp, - onNavigateToAppDetails = { packageName -> - backstack.add(Screen.AppDetails(packageName)) - } + onNavigateTo = ::navigate ) } entry { screen -> PermissionRationaleScreen( - onNavigateUp = ::onNavigateUp, requiredPermissions = screen.requiredPermissions ) } - entry { - DownloadsScreen( - onNavigateUp = ::onNavigateUp, - onNavigateToAppDetails = { packageName -> - backstack.add(Screen.AppDetails(packageName)) - } - ) - } - - entry { - AccountsScreen( - onNavigateUp = ::onNavigateUp, - onNavigateToSplash = { activity?.startActivity(splashIntent) } - ) - } - - entry { - AboutScreen(onNavigateUp = ::onNavigateUp) - } - - entry { - FavouriteScreen( - onNavigateUp = ::onNavigateUp, - onNavigateToAppDetails = { packageName -> - backstack.add(Screen.AppDetails(packageName)) - } - ) - } - - entry { - OnboardingScreen() - } - - entry { - SpoofScreen( - onNavigateUp = ::onNavigateUp, - onNavigateToSplash = { activity?.startActivity(splashIntent) } - ) - } - - entry { - DispenserScreen(onNavigateUp = ::onNavigateUp) - } - - entry { - InstallerScreen(onNavigateUp = ::onNavigateUp) - } - - entry { - InstalledScreen( - onNavigateUp = ::onNavigateUp, - onNavigateToAppDetails = { packageName -> - backstack.add(Screen.AppDetails(packageName)) - } - ) - } - entry { screen -> StreamBrowseScreen( streamCluster = screen.streamCluster, - onNavigateUp = ::onNavigateUp, - onNavigateToAppDetails = { packageName -> - backstack.add(Screen.AppDetails(packageName)) - } + onNavigateTo = ::navigate ) } + + entry { screen -> + ExpandedStreamBrowseScreen( + browseUrl = screen.browseUrl, + defaultTitle = screen.title, + onNavigateTo = ::navigate + ) + } + + entry { screen -> + CategoryBrowseScreen( + title = screen.title, + browseUrl = screen.browseUrl, + onNavigateTo = ::navigate + ) + } + + entry { + InstallationPreferenceScreen(onNavigateTo = ::navigate) + } + + entry( + metadata = metadata { + put(NavDisplay.TransitionKey) { + fadeIn() togetherWith + ExitTransition.KeepUntilTransitionsFinished + } + put(NavDisplay.PopTransitionKey) { + EnterTransition.None togetherWith + slideOutVertically(targetOffsetY = { it }) + } + put(NavDisplay.PredictivePopTransitionKey) { + EnterTransition.None togetherWith + slideOutVertically(targetOffsetY = { it }) + } + } + ) { SearchScreen() } + + entry { SplashScreen(onNavigateTo = ::navigate) } + entry { OnboardingScreen() } + entry { BlacklistScreen() } + entry { DownloadsScreen(onNavigateTo = ::navigate) } + entry { AccountsScreen(onNavigateTo = ::navigate) } + entry { GoogleLoginScreen(onNavigateTo = ::navigate) } + entry { AboutScreen() } + entry { FavouriteScreen(onNavigateTo = ::navigate) } + entry { SpoofScreen(onNavigateTo = ::navigate) } + entry { DispenserScreen() } + entry { InstallerScreen() } + entry { InstalledScreen(onNavigateTo = ::navigate) } + entry { SettingsScreen(onNavigateTo = ::navigate) } + entry { NetworkPreferenceScreen(onNavigateTo = ::navigate) } + entry { UIPreferenceScreen() } + entry { UpdatesPreferenceScreen() } } ) } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt index be2989737..c651ab0d3 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt @@ -44,6 +44,9 @@ sealed class Screen : NavKey, Parcelable { @Serializable data object Accounts : Screen() + @Serializable + data object GoogleLogin : Screen() + @Serializable data object About : Screen() @@ -67,4 +70,31 @@ sealed class Screen : NavKey, Parcelable { @Serializable data class StreamBrowse(val streamCluster: StreamCluster) : Screen() + + @Serializable + data class ExpandedStreamBrowse(val title: String, val browseUrl: String) : Screen() + + @Serializable + data class CategoryBrowse(val title: String, val browseUrl: String) : Screen() + + @Serializable + data object Settings : Screen() + + @Serializable + data object InstallationPreference : Screen() + + @Serializable + data object NetworkPreference : Screen() + + @Serializable + data object UIPreference : Screen() + + @Serializable + data object UpdatesPreference : Screen() + + @Serializable + data object Splash : Screen() + + @Serializable + data class Main(val initialTab: Int = 0) : Screen() } diff --git a/app/src/main/java/com/aurora/store/compose/theme/Theme.kt b/app/src/main/java/com/aurora/store/compose/theme/Theme.kt index 3f6365fda..1b5d8cbb7 100644 --- a/app/src/main/java/com/aurora/store/compose/theme/Theme.kt +++ b/app/src/main/java/com/aurora/store/compose/theme/Theme.kt @@ -6,6 +6,7 @@ package com.aurora.store.compose.theme +import android.content.SharedPreferences import android.os.Build import androidx.activity.compose.LocalActivity import androidx.compose.foundation.isSystemInDarkTheme @@ -16,7 +17,12 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.colorResource @@ -31,7 +37,19 @@ import com.aurora.store.util.Preferences fun AuroraTheme(content: @Composable () -> Unit) { val context = LocalContext.current - val themeStyle = Preferences.getInteger(context, Preferences.PREFERENCE_THEME_STYLE) + var themeStyle by remember { + mutableIntStateOf(Preferences.getInteger(context, Preferences.PREFERENCE_THEME_STYLE)) + } + DisposableEffect(Unit) { + val prefs = Preferences.getPrefs(context) + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == Preferences.PREFERENCE_THEME_STYLE) { + themeStyle = Preferences.getInteger(context, Preferences.PREFERENCE_THEME_STYLE) + } + } + prefs.registerOnSharedPreferenceChangeListener(listener) + onDispose { prefs.unregisterOnSharedPreferenceChangeListener(listener) } + } val isDynamicColorSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val lightScheme = if (isDynamicColorSupported) { @@ -69,11 +87,9 @@ fun AuroraTheme(content: @Composable () -> Unit) { val currentActivity = activity ?: return@SideEffect val window = currentActivity.window - // Transparent system bars window.statusBarColor = android.graphics.Color.TRANSPARENT window.navigationBarColor = android.graphics.Color.TRANSPARENT - // Control icon colors explicitly WindowCompat .getInsetsController(window, view) .apply { From 8d3eb1176e7df0891de912628af8b2b3e9d1430d Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Tue, 19 May 2026 12:11:23 +0530 Subject: [PATCH 05/27] 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. --- .../store/compose/ui/apps/AppsGamesScreen.kt | 127 +++++++++ .../store/compose/ui/apps/CategoriesPage.kt | 87 ++++++ .../store/compose/ui/apps/ForYouPage.kt | 50 ++++ .../store/compose/ui/apps/TopChartsPage.kt | 255 ++++++++++++++++++ .../store/compose/ui/main/MainScreen.kt | 240 +++++++++++++++++ 5 files changed, 759 insertions(+) create mode 100644 app/src/main/java/com/aurora/store/compose/ui/apps/AppsGamesScreen.kt create mode 100644 app/src/main/java/com/aurora/store/compose/ui/apps/CategoriesPage.kt create mode 100644 app/src/main/java/com/aurora/store/compose/ui/apps/ForYouPage.kt create mode 100644 app/src/main/java/com/aurora/store/compose/ui/apps/TopChartsPage.kt create mode 100644 app/src/main/java/com/aurora/store/compose/ui/main/MainScreen.kt 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() } + ) + } + } + } + } +} From 9a1191ecf8f56a031ab24fa1a6e0792e90ae1530 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Tue, 19 May 2026 12:11:41 +0530 Subject: [PATCH 06/27] compose: add UpdatesScreen with pull-to-refresh Renders the cached update list, exposes per-row update / cancel actions, and supports the bulk Update-All header. Pull-to-refresh triggers an expedited UpdateWorker check. --- .../store/compose/ui/updates/UpdatesScreen.kt | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 app/src/main/java/com/aurora/store/compose/ui/updates/UpdatesScreen.kt diff --git a/app/src/main/java/com/aurora/store/compose/ui/updates/UpdatesScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/updates/UpdatesScreen.kt new file mode 100644 index 000000000..34e3425b7 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/updates/UpdatesScreen.kt @@ -0,0 +1,127 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.updates + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.aurora.store.R +import com.aurora.store.compose.composable.EmptyState +import com.aurora.store.compose.composable.SectionHeaderWithAction +import com.aurora.store.compose.composable.ShimmerUpdateItem +import com.aurora.store.compose.composable.app.AppUpdateItem +import com.aurora.store.compose.navigation.Destination +import com.aurora.store.data.model.MinimalApp +import com.aurora.store.data.room.update.Update +import com.aurora.store.viewmodel.all.UpdatesViewModel + +@Composable +fun UpdatesScreen( + viewModel: UpdatesViewModel = hiltViewModel(), + onNavigateTo: (Destination) -> Unit = {}, + onRequestUpdate: (Update) -> Unit = {}, + onRequestUpdateAll: () -> Unit = {}, + onCancelUpdate: (String) -> Unit = {}, + onCancelAll: () -> Unit = {} +) { + val updates by viewModel.updates.collectAsStateWithLifecycle() + val downloads by viewModel.downloadsList.collectAsStateWithLifecycle() + val fetchingUpdates by viewModel.fetchingUpdates.collectAsStateWithLifecycle() + + val updateMap = remember(updates, downloads) { + updates?.associateWith { update -> + downloads.find { + it.packageName == update.packageName && + it.versionCode == update.versionCode + } + } + } + val allEnqueued = updateMap?.isNotEmpty() == true && + updateMap.values.all { it?.isRunning == true } + + PullToRefreshBox( + modifier = Modifier.fillMaxSize(), + isRefreshing = fetchingUpdates, + onRefresh = { viewModel.fetchUpdates() } + ) { + when { + updateMap == null -> { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy( + dimensionResource(R.dimen.margin_medium) + ) + ) { + items(10) { ShimmerUpdateItem() } + } + } + + updateMap.isEmpty() -> { + EmptyState( + modifier = Modifier.fillMaxSize(), + icon = R.drawable.ic_updates, + message = R.string.details_no_updates, + actionLabel = R.string.check_updates, + onAction = { viewModel.fetchUpdates() } + ) + } + + else -> { + val title = "${updateMap.size} " + stringResource( + if (updateMap.size == 1) { + R.string.update_available + } else { + R.string.updates_available + } + ) + val actionLabel = stringResource( + if (allEnqueued) R.string.action_cancel else R.string.action_update_all + ) + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy( + dimensionResource(R.dimen.margin_medium) + ) + ) { + item(key = "header") { + SectionHeaderWithAction( + title = title, + action = actionLabel, + onAction = { + if (allEnqueued) onCancelAll() else onRequestUpdateAll() + } + ) + } + items( + items = updateMap.entries.toList(), + key = { it.key.packageName } + ) { (update, download) -> + AppUpdateItem( + update = update, + download = download, + onClick = { onNavigateTo(Destination.AppDetails(update.packageName)) }, + onLongClick = { + onNavigateTo(Destination.AppMenu(MinimalApp.fromUpdate(update))) + }, + onUpdate = { onRequestUpdate(update) }, + onCancel = { onCancelUpdate(update.packageName) } + ) + } + } + } + } + } +} From 9e0a4ffee79d72eb59cda50a861746a120549c95 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Tue, 19 May 2026 12:12:01 +0530 Subject: [PATCH 07/27] compose: migrate preferences screens Settings, UI, Installation, Network and Updates preference panes are now native Compose. Replaces the old PreferenceFragment XML hierarchy with explicit ListItem / Switch / dialog rows. --- .../compose/ui/preferences/SettingsScreen.kt | 124 +++++ .../ui/preferences/UIPreferenceScreen.kt | 178 +++++++ .../InstallationPreferenceScreen.kt | 156 ++++++ .../installation/InstallerScreen.kt | 7 +- .../network/NetworkPreferenceScreen.kt | 316 ++++++++++++ .../updates/UpdatesPreferenceScreen.kt | 456 ++++++++++++++++++ 6 files changed, 1232 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/aurora/store/compose/ui/preferences/SettingsScreen.kt create mode 100644 app/src/main/java/com/aurora/store/compose/ui/preferences/UIPreferenceScreen.kt create mode 100644 app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallationPreferenceScreen.kt create mode 100644 app/src/main/java/com/aurora/store/compose/ui/preferences/network/NetworkPreferenceScreen.kt create mode 100644 app/src/main/java/com/aurora/store/compose/ui/preferences/updates/UpdatesPreferenceScreen.kt diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/SettingsScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/SettingsScreen.kt new file mode 100644 index 000000000..a623e2a77 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/SettingsScreen.kt @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.preferences + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import com.aurora.store.R +import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.navigation.Destination +import com.aurora.store.compose.preview.ThemePreviewProvider +import com.aurora.store.data.model.PermissionType + +@Composable +fun SettingsScreen(onNavigateTo: (Destination) -> Unit) { + ScreenContent(onNavigateTo = onNavigateTo) +} + +@Composable +private fun ScreenContent(onNavigateTo: (Destination) -> Unit = {}) { + Scaffold( + topBar = { + TopAppBar( + title = stringResource(R.string.title_settings) + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + item { + ListItem( + modifier = Modifier.clickable { + onNavigateTo( + Destination.PermissionRationale(PermissionType.entries.toSet()) + ) + }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_list_check), + contentDescription = null + ) + }, + headlineContent = { + Text(stringResource(R.string.onboarding_title_permissions)) + } + ) + } + item { + ListItem( + modifier = Modifier.clickable { + onNavigateTo(Destination.InstallationPreference) + }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_installation), + contentDescription = null + ) + }, + headlineContent = { Text(stringResource(R.string.title_installation)) } + ) + } + item { + ListItem( + modifier = Modifier.clickable { onNavigateTo(Destination.UIPreference) }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_ui), + contentDescription = null + ) + }, + headlineContent = { Text(stringResource(R.string.pref_ui_title)) } + ) + } + item { + ListItem( + modifier = Modifier.clickable { onNavigateTo(Destination.NetworkPreference) }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_network), + contentDescription = null + ) + }, + headlineContent = { Text(stringResource(R.string.pref_network_title)) } + ) + } + item { + ListItem( + modifier = Modifier.clickable { onNavigateTo(Destination.UpdatesPreference) }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_updates), + contentDescription = null + ) + }, + headlineContent = { Text(stringResource(R.string.title_updates)) } + ) + } + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun SettingsScreenPreview() { + ScreenContent() +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/UIPreferenceScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/UIPreferenceScreen.kt new file mode 100644 index 000000000..872400775 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/UIPreferenceScreen.kt @@ -0,0 +1,178 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.preferences + +import android.content.Intent +import android.provider.Settings +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLocale +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.core.net.toUri +import com.aurora.extensions.isTAndAbove +import com.aurora.extensions.setAppTheme +import com.aurora.store.R +import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.preview.ThemePreviewProvider +import com.aurora.store.compose.ui.preferences.network.SingleChoiceDialog +import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB +import com.aurora.store.util.Preferences.PREFERENCE_FOR_YOU +import com.aurora.store.util.Preferences.PREFERENCE_THEME_STYLE +import com.aurora.store.util.save + +@Composable +fun UIPreferenceScreen() { + ScreenContent() +} + +@Composable +private fun ScreenContent() { + val context = LocalContext.current + + val themeEntries = stringArrayResource(R.array.pref_theme_style) + var themeStyle by remember { + mutableIntStateOf(Preferences.getInteger(context, PREFERENCE_THEME_STYLE)) + } + val tabEntries = stringArrayResource(R.array.pref_default_tab) + var selectedTab by remember { + mutableIntStateOf(Preferences.getInteger(context, PREFERENCE_DEFAULT_SELECTED_TAB)) + } + var forYou by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_FOR_YOU, true)) + } + var showThemeDialog by remember { mutableStateOf(false) } + var showTabDialog by remember { mutableStateOf(false) } + + if (showThemeDialog) { + SingleChoiceDialog( + title = stringResource(R.string.pref_ui_theme), + options = themeEntries.toList(), + selected = themeStyle, + onSelect = { index -> + themeStyle = index + context.save(PREFERENCE_THEME_STYLE, index) + setAppTheme(index) + showThemeDialog = false + }, + onDismiss = { showThemeDialog = false } + ) + } + + if (showTabDialog) { + SingleChoiceDialog( + title = stringResource(R.string.pref_ui_layout_tab), + options = tabEntries.toList(), + selected = selectedTab, + onSelect = { index -> + selectedTab = index + context.save(PREFERENCE_DEFAULT_SELECTED_TAB, index) + showTabDialog = false + }, + onDismiss = { showTabDialog = false } + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = stringResource(R.string.pref_ui_title) + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + if (isTAndAbove) { + item { + ListItem( + modifier = Modifier.clickable { + context.startActivity( + Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply { + data = ("package:" + context.packageName).toUri() + } + ) + }, + headlineContent = { Text(stringResource(R.string.app_language)) }, + supportingContent = { Text(LocalLocale.current.platformLocale.displayName) } + ) + } + } + item { HorizontalDivider() } + item { + ListItem(headlineContent = { Text(stringResource(R.string.pref_ui_theme)) }) + } + item { + ListItem( + modifier = Modifier.clickable { showThemeDialog = true }, + headlineContent = { Text(stringResource(R.string.pref_ui_theme)) }, + supportingContent = { Text(themeEntries.getOrElse(themeStyle) { "" }) } + ) + } + item { HorizontalDivider() } + item { + ListItem(headlineContent = { Text(stringResource(R.string.pref_ui_layout)) }) + } + item { + ListItem( + modifier = Modifier.clickable { showTabDialog = true }, + headlineContent = { Text(stringResource(R.string.pref_ui_layout_tab)) }, + supportingContent = { Text(tabEntries.getOrElse(selectedTab) { "" }) } + ) + } + item { HorizontalDivider() } + item { + ListItem(headlineContent = { Text(stringResource(R.string.pref_common_extra)) }) + } + item { + ListItem( + modifier = Modifier.clickable { + forYou = !forYou + context.save(PREFERENCE_FOR_YOU, forYou) + }, + headlineContent = { Text(stringResource(R.string.pref_ui_no_for_you)) }, + supportingContent = { Text(stringResource(R.string.pref_ui_no_for_you_desc)) }, + trailingContent = { + Switch( + checked = forYou, + onCheckedChange = { checked -> + forYou = checked + context.save(PREFERENCE_FOR_YOU, checked) + } + ) + } + ) + } + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun UIPreferenceScreenPreview() { + ScreenContent() +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallationPreferenceScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallationPreferenceScreen.kt new file mode 100644 index 000000000..1023f45b1 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallationPreferenceScreen.kt @@ -0,0 +1,156 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.preferences.installation + +import android.app.admin.DevicePolicyManager +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.core.content.getSystemService +import com.aurora.store.R +import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.navigation.Destination +import com.aurora.store.compose.preview.ThemePreviewProvider +import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE +import com.aurora.store.util.save + +@Composable +fun InstallationPreferenceScreen(onNavigateTo: (Destination) -> Unit) { + val context = LocalContext.current + val devicePolicyManager = context.getSystemService() + val isDeviceOwner = devicePolicyManager?.isDeviceOwnerApp(context.packageName) ?: false + + ScreenContent( + onNavigateTo = onNavigateTo, + isDeviceOwner = isDeviceOwner, + onClearDeviceOwner = { + @Suppress("DEPRECATION") + devicePolicyManager?.clearDeviceOwnerApp(context.packageName) + } + ) +} + +@Composable +private fun ScreenContent( + onNavigateTo: (Destination) -> Unit = {}, + isDeviceOwner: Boolean = false, + onClearDeviceOwner: () -> Unit = {} +) { + val context = LocalContext.current + var autoDelete by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_AUTO_DELETE, true)) + } + var showClearOwnerDialog by remember { mutableStateOf(false) } + + if (showClearOwnerDialog) { + AlertDialog( + onDismissRequest = { showClearOwnerDialog = false }, + title = { Text(stringResource(R.string.pref_clear_device_owner_title)) }, + text = { Text(stringResource(R.string.pref_clear_device_owner_desc)) }, + confirmButton = { + TextButton(onClick = { + showClearOwnerDialog = false + onClearDeviceOwner() + }) { + Text(stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = { showClearOwnerDialog = false }) { + Text(stringResource(android.R.string.cancel)) + } + } + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = stringResource(R.string.title_installation) + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + item { + ListItem( + modifier = Modifier.clickable { onNavigateTo(Destination.Installer) }, + headlineContent = { Text(stringResource(R.string.pref_install_mode_title)) }, + supportingContent = { Text(stringResource(R.string.pref_install_mode_summary)) } + ) + } + item { HorizontalDivider() } + item { + ListItem( + headlineContent = { Text(stringResource(R.string.pref_common_extra)) } + ) + } + item { + ListItem( + modifier = Modifier.clickable { + autoDelete = !autoDelete + context.save(PREFERENCE_AUTO_DELETE, autoDelete) + }, + headlineContent = { Text(stringResource(R.string.pref_install_delete_title)) }, + supportingContent = { + Text(stringResource(R.string.pref_install_delete_summary)) + }, + trailingContent = { + Switch( + checked = autoDelete, + onCheckedChange = { checked -> + autoDelete = checked + context.save(PREFERENCE_AUTO_DELETE, checked) + } + ) + } + ) + } + if (isDeviceOwner) { + item { + ListItem( + modifier = Modifier.clickable { showClearOwnerDialog = true }, + headlineContent = { + Text(stringResource(R.string.pref_clear_device_owner_title)) + }, + supportingContent = { + Text(stringResource(R.string.pref_clear_device_owner_summary)) + } + ) + } + } + } + } +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun InstallationPreferenceScreenPreview() { + ScreenContent() +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallerScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallerScreen.kt index 2292fd4db..ef0cb9b62 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallerScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/installation/InstallerScreen.kt @@ -35,7 +35,7 @@ import com.aurora.store.data.model.InstallerInfo import com.aurora.store.viewmodel.preferences.InstallerViewModel @Composable -fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hiltViewModel()) { +fun InstallerScreen(viewModel: InstallerViewModel = hiltViewModel()) { val currentInstallerId by viewModel.currentInstaller.collectAsStateWithLifecycle() val snackBarHostState = remember { SnackbarHostState() } @@ -46,7 +46,6 @@ fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hi } ScreenContent( - onNavigateUp = onNavigateUp, snackBarHostState = snackBarHostState, currentInstaller = Installer.entries[currentInstallerId], availableInstallers = AppInstaller.getAvailableInstallersInfo(LocalContext.current), @@ -56,7 +55,6 @@ fun InstallerScreen(onNavigateUp: () -> Unit, viewModel: InstallerViewModel = hi @Composable private fun ScreenContent( - onNavigateUp: () -> Unit = {}, snackBarHostState: SnackbarHostState = SnackbarHostState(), currentInstaller: Installer = Installer.SESSION, availableInstallers: List = emptyList(), @@ -70,8 +68,7 @@ private fun ScreenContent( }, topBar = { TopAppBar( - title = stringResource(R.string.pref_install_mode_title), - onNavigateUp = onNavigateUp + title = stringResource(R.string.pref_install_mode_title) ) } ) { paddingValues -> diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/network/NetworkPreferenceScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/network/NetworkPreferenceScreen.kt new file mode 100644 index 000000000..cd0f0653d --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/network/NetworkPreferenceScreen.kt @@ -0,0 +1,316 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.preferences.network + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.aurora.store.R +import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.navigation.Destination +import com.aurora.store.compose.preview.ThemePreviewProvider +import com.aurora.store.compose.ui.commons.ForceRestartDialog +import com.aurora.store.util.CommonUtil +import com.aurora.store.util.PackageUtil +import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCE_MICROG_AUTH +import com.aurora.store.util.Preferences.PREFERENCE_PROXY_INFO +import com.aurora.store.util.Preferences.PREFERENCE_PROXY_URL +import com.aurora.store.util.Preferences.PREFERENCE_VENDING_VERSION +import com.aurora.store.util.remove +import com.aurora.store.util.save +import com.aurora.store.viewmodel.preferences.ProxyURLViewModel +import com.jakewharton.processphoenix.ProcessPhoenix + +@Composable +fun NetworkPreferenceScreen( + onNavigateTo: (Destination) -> Unit, + viewModel: ProxyURLViewModel = hiltViewModel() +) { + val context = LocalContext.current + val hasMicroG = PackageUtil.hasSupportedMicroGVariant(context) + + ScreenContent( + onNavigateTo = onNavigateTo, + hasMicroG = hasMicroG, + onSaveProxyUrl = { url -> + val proxyInfo = CommonUtil.parseProxyUrl(url) + if (proxyInfo != null) { + context.save(PREFERENCE_PROXY_URL, url) + context.save(PREFERENCE_PROXY_INFO, viewModel.json.encodeToString(proxyInfo)) + true + } else { + false + } + }, + onDeleteProxyUrl = { + Preferences.remove(context, PREFERENCE_PROXY_URL) + Preferences.remove(context, PREFERENCE_PROXY_INFO) + } + ) +} + +@Composable +private fun ScreenContent( + onNavigateTo: (Destination) -> Unit = {}, + hasMicroG: Boolean = false, + onSaveProxyUrl: (String) -> Boolean = { false }, + onDeleteProxyUrl: () -> Unit = {} +) { + val context = LocalContext.current + + val vendingEntries = stringArrayResource(R.array.pref_vending_version) + var vendingVersion by remember { + mutableIntStateOf(Preferences.getInteger(context, PREFERENCE_VENDING_VERSION)) + } + var microGAuth by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_MICROG_AUTH, true)) + } + var showProxyDialog by remember { mutableStateOf(false) } + var showVendingDialog by remember { mutableStateOf(false) } + var showForceRestartDialog by remember { mutableStateOf(false) } + + if (showForceRestartDialog) { + ForceRestartDialog( + onConfirm = { ProcessPhoenix.triggerRebirth(context) }, + onDismiss = { showForceRestartDialog = false } + ) + } + + if (showProxyDialog) { + ProxyURLDialog( + currentUrl = Preferences.getString(context, PREFERENCE_PROXY_URL), + onSave = { url -> + showProxyDialog = false + if (onSaveProxyUrl(url)) { + showForceRestartDialog = true + } + }, + onDelete = { + showProxyDialog = false + onDeleteProxyUrl() + showForceRestartDialog = true + }, + onDismiss = { showProxyDialog = false } + ) + } + + if (showVendingDialog) { + SingleChoiceDialog( + title = stringResource(R.string.pref_vending_version_title), + options = vendingEntries.toList(), + selected = vendingVersion, + onSelect = { index -> + vendingVersion = index + context.save(PREFERENCE_VENDING_VERSION, index) + showVendingDialog = false + }, + onDismiss = { showVendingDialog = false } + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = stringResource(R.string.pref_network_title) + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + item { + ListItem( + modifier = Modifier.clickable { onNavigateTo(Destination.Dispenser) }, + headlineContent = { Text(stringResource(R.string.pref_dispenser_title)) }, + supportingContent = { Text(stringResource(R.string.pref_dispenser_summary)) } + ) + } + item { + ListItem( + modifier = Modifier.clickable { showProxyDialog = true }, + headlineContent = { Text(stringResource(R.string.pref_network_proxy_url)) }, + supportingContent = { Text(stringResource(R.string.pref_network_proxy_desc)) } + ) + } + item { HorizontalDivider() } + item { + ListItem(headlineContent = { Text(stringResource(R.string.pref_common_extra)) }) + } + if (hasMicroG) { + item { + ListItem( + modifier = Modifier.clickable { + microGAuth = !microGAuth + context.save(PREFERENCE_MICROG_AUTH, microGAuth) + }, + headlineContent = { + Text(stringResource(R.string.pref_network_microg_login_title)) + }, + supportingContent = { + Text(stringResource(R.string.pref_network_microg_login_desc)) + }, + trailingContent = { + Switch( + checked = microGAuth, + onCheckedChange = { checked -> + microGAuth = checked + context.save(PREFERENCE_MICROG_AUTH, checked) + } + ) + } + ) + } + } + item { + ListItem( + modifier = Modifier.clickable { showVendingDialog = true }, + headlineContent = { Text(stringResource(R.string.pref_vending_version_title)) }, + supportingContent = { Text(vendingEntries.getOrElse(vendingVersion) { "" }) } + ) + } + } + } +} + +@Composable +private fun ProxyURLDialog( + currentUrl: String, + onSave: (String) -> Unit, + onDelete: () -> Unit, + onDismiss: () -> Unit +) { + var url by remember { mutableStateOf(currentUrl) } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.pref_network_proxy_url)) }, + text = { + Column { + Text( + text = stringResource(R.string.pref_network_proxy_url_message), + modifier = Modifier.padding(bottom = dimensionResource(R.dimen.padding_small)) + ) + OutlinedTextField( + value = url, + onValueChange = { url = it }, + label = { Text(stringResource(R.string.pref_network_proxy_url_hint)) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), + modifier = Modifier.fillMaxWidth() + ) + } + }, + confirmButton = { + TextButton( + enabled = url.isNotBlank(), + onClick = { onSave(url.trim()) } + ) { + Text(stringResource(R.string.set)) + } + }, + dismissButton = { + Row { + if (currentUrl.isNotBlank()) { + TextButton(onClick = onDelete) { + Text(stringResource(R.string.disable)) + } + } + Spacer(Modifier.width(dimensionResource(R.dimen.margin_small))) + TextButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + } + } + ) +} + +@Composable +internal fun SingleChoiceDialog( + title: String, + options: List, + selected: Int, + onSelect: (Int) -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { + Column(modifier = Modifier.selectableGroup()) { + options.forEachIndexed { index, option -> + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = index == selected, + onClick = { onSelect(index) }, + role = Role.RadioButton + ) + .padding(vertical = dimensionResource(R.dimen.padding_small)), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = index == selected, + onClick = null + ) + Spacer(Modifier.width(dimensionResource(R.dimen.margin_small))) + Text(option) + } + } + } + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + } + ) +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun NetworkPreferenceScreenPreview() { + ScreenContent(hasMicroG = true) +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/updates/UpdatesPreferenceScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/updates/UpdatesPreferenceScreen.kt new file mode 100644 index 000000000..7d76ec05c --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/updates/UpdatesPreferenceScreen.kt @@ -0,0 +1,456 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.preferences.updates + +import android.Manifest +import android.content.Intent +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewWrapper +import androidx.core.net.toUri +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.aurora.extensions.isIgnoringBatteryOptimizations +import com.aurora.extensions.isTAndAbove +import com.aurora.store.R +import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.preview.ThemePreviewProvider +import com.aurora.store.compose.ui.preferences.network.SingleChoiceDialog +import com.aurora.store.data.model.UpdateMode +import com.aurora.store.util.Preferences +import com.aurora.store.util.Preferences.PREFERENCES_UPDATES_RESTRICTIONS_BATTERY +import com.aurora.store.util.Preferences.PREFERENCES_UPDATES_RESTRICTIONS_IDLE +import com.aurora.store.util.Preferences.PREFERENCES_UPDATES_RESTRICTIONS_METERED +import com.aurora.store.util.Preferences.PREFERENCE_FILTER_AURORA_ONLY +import com.aurora.store.util.Preferences.PREFERENCE_FILTER_FDROID +import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO +import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_CHECK_INTERVAL +import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED +import com.aurora.store.util.save +import com.aurora.store.viewmodel.all.UpdatesViewModel +import kotlin.math.roundToInt + +@Composable +fun UpdatesPreferenceScreen(viewModel: UpdatesViewModel = hiltViewModel()) { + ScreenContent( + onCancelAutomatedCheck = { viewModel.updateHelper.cancelAutomatedCheck() }, + onScheduleAutomatedCheck = { viewModel.updateHelper.scheduleAutomatedCheck() }, + onUpdateAutomatedCheck = { viewModel.updateHelper.updateAutomatedCheck() }, + onCheckUpdatesNow = { viewModel.updateHelper.checkUpdatesNow() } + ) +} + +@Composable +private fun ScreenContent( + onCancelAutomatedCheck: () -> Unit = {}, + onScheduleAutomatedCheck: () -> Unit = {}, + onUpdateAutomatedCheck: () -> Unit = {}, + onCheckUpdatesNow: () -> Unit = {} +) { + val context = LocalContext.current + val autoEntries = stringArrayResource(R.array.pref_updates_auto) + + var autoMode by remember { + mutableIntStateOf(Preferences.getInteger(context, PREFERENCE_UPDATES_AUTO, 2)) + } + val autoEnabled = autoMode != UpdateMode.DISABLED.ordinal + + var checkInterval by remember { + mutableFloatStateOf( + Preferences.getInteger(context, PREFERENCE_UPDATES_CHECK_INTERVAL, 3).toFloat() + ) + } + var filterFDroid by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_FILTER_FDROID, true)) + } + var filterAuroraOnly by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_FILTER_AURORA_ONLY)) + } + var updatesExtended by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCE_UPDATES_EXTENDED)) + } + var showAutoDialog by remember { mutableStateOf(false) } + var showRestrictionsDialog by remember { mutableStateOf(false) } + + val notifPermissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + if (granted) { + context.save(PREFERENCE_UPDATES_AUTO, UpdateMode.CHECK_AND_NOTIFY.ordinal) + autoMode = UpdateMode.CHECK_AND_NOTIFY.ordinal + onScheduleAutomatedCheck() + } else { + autoMode = Preferences.getInteger(context, PREFERENCE_UPDATES_AUTO, 2) + } + } + + val batteryOptLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (context.isIgnoringBatteryOptimizations()) { + context.save(PREFERENCE_UPDATES_AUTO, UpdateMode.CHECK_AND_INSTALL.ordinal) + autoMode = UpdateMode.CHECK_AND_INSTALL.ordinal + onScheduleAutomatedCheck() + } else { + autoMode = Preferences.getInteger(context, PREFERENCE_UPDATES_AUTO, 2) + } + } + + if (showAutoDialog) { + SingleChoiceDialog( + title = stringResource(R.string.pref_updates_auto), + options = autoEntries.toList(), + selected = autoMode, + onSelect = { index -> + showAutoDialog = false + when (UpdateMode.entries[index]) { + UpdateMode.DISABLED -> { + context.save(PREFERENCE_UPDATES_AUTO, 0) + autoMode = 0 + onCancelAutomatedCheck() + } + UpdateMode.CHECK_AND_NOTIFY -> { + if (!isTAndAbove || + context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == + android.content.pm.PackageManager.PERMISSION_GRANTED + ) { + context.save(PREFERENCE_UPDATES_AUTO, 1) + autoMode = 1 + onScheduleAutomatedCheck() + } else { + notifPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + UpdateMode.CHECK_AND_INSTALL -> { + if (context.isIgnoringBatteryOptimizations()) { + context.save(PREFERENCE_UPDATES_AUTO, 2) + autoMode = 2 + onScheduleAutomatedCheck() + } else { + batteryOptLauncher.launch( + Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + "package:${context.packageName}".toUri() + ) + ) + } + } + else -> {} + } + }, + onDismiss = { showAutoDialog = false } + ) + } + + if (showRestrictionsDialog) { + UpdatesRestrictionsDialog( + onUpdateAutomatedCheck = onUpdateAutomatedCheck, + onDismiss = { showRestrictionsDialog = false } + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = stringResource(R.string.title_updates) + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + item { + ListItem( + modifier = Modifier.clickable { showAutoDialog = true }, + headlineContent = { Text(stringResource(R.string.pref_updates_auto)) }, + supportingContent = { Text(autoEntries.getOrElse(autoMode) { "" }) } + ) + } + if (autoEnabled) { + item { + ListItem( + headlineContent = { + Text(stringResource(R.string.pref_updates_check_frequency)) + }, + supportingContent = { + Column { + Text(stringResource(R.string.pref_updates_check_frequency_desc)) + Spacer(Modifier.height(dimensionResource(R.dimen.margin_small))) + Row(verticalAlignment = Alignment.CenterVertically) { + Slider( + value = checkInterval, + onValueChange = { checkInterval = it }, + onValueChangeFinished = { + context.save( + PREFERENCE_UPDATES_CHECK_INTERVAL, + checkInterval.roundToInt() + ) + onUpdateAutomatedCheck() + }, + valueRange = 1f..24f, + steps = 22, + modifier = Modifier.weight(1f) + ) + Spacer(Modifier.width(dimensionResource(R.dimen.margin_small))) + Text(checkInterval.roundToInt().toString()) + } + } + } + ) + } + item { + ListItem( + modifier = Modifier.clickable { showRestrictionsDialog = true }, + headlineContent = { + Text(stringResource(R.string.pref_updates_restrictions_title)) + }, + supportingContent = { + Text(stringResource(R.string.pref_updates_restrictions_desc)) + } + ) + } + } + item { HorizontalDivider() } + item { + ListItem(headlineContent = { + Text(stringResource(R.string.pref_updates_app_source)) + }) + } + item { + ListItem( + modifier = Modifier.clickable { + filterFDroid = !filterFDroid + context.save(PREFERENCE_FILTER_FDROID, filterFDroid) + onCheckUpdatesNow() + }, + headlineContent = { Text(stringResource(R.string.pref_filter_fdroid_title)) }, + supportingContent = { + Text(stringResource(R.string.pref_filter_fdroid_summary)) + }, + trailingContent = { + Switch( + checked = filterFDroid, + onCheckedChange = { checked -> + filterFDroid = checked + context.save(PREFERENCE_FILTER_FDROID, checked) + onCheckUpdatesNow() + } + ) + } + ) + } + item { + ListItem( + modifier = Modifier.clickable { + filterAuroraOnly = !filterAuroraOnly + context.save(PREFERENCE_FILTER_AURORA_ONLY, filterAuroraOnly) + if (filterAuroraOnly) filterFDroid = false + onCheckUpdatesNow() + }, + headlineContent = { Text(stringResource(R.string.pref_aurora_only)) }, + supportingContent = { Text(stringResource(R.string.pref_aurora_only_desc)) }, + trailingContent = { + Switch( + checked = filterAuroraOnly, + onCheckedChange = { checked -> + filterAuroraOnly = checked + context.save(PREFERENCE_FILTER_AURORA_ONLY, checked) + if (checked) filterFDroid = false + onCheckUpdatesNow() + } + ) + } + ) + } + item { HorizontalDivider() } + item { + ListItem(headlineContent = { Text(stringResource(R.string.pref_common_advanced)) }) + } + item { + ListItem( + modifier = Modifier.clickable { + updatesExtended = !updatesExtended + context.save(PREFERENCE_UPDATES_EXTENDED, updatesExtended) + onCheckUpdatesNow() + }, + headlineContent = { Text(stringResource(R.string.pref_updates_incompatible)) }, + supportingContent = { + Text(stringResource(R.string.pref_updates_incompatible_desc)) + }, + trailingContent = { + Switch( + checked = updatesExtended, + onCheckedChange = { checked -> + updatesExtended = checked + context.save(PREFERENCE_UPDATES_EXTENDED, checked) + onCheckUpdatesNow() + } + ) + } + ) + } + } + } +} + +@Composable +private fun UpdatesRestrictionsDialog(onUpdateAutomatedCheck: () -> Unit, onDismiss: () -> Unit) { + val context = LocalContext.current + var metered by remember { + mutableStateOf( + Preferences.getBoolean(context, PREFERENCES_UPDATES_RESTRICTIONS_METERED, true) + ) + } + var idle by remember { + mutableStateOf(Preferences.getBoolean(context, PREFERENCES_UPDATES_RESTRICTIONS_IDLE, true)) + } + var battery by remember { + mutableStateOf( + Preferences.getBoolean(context, PREFERENCES_UPDATES_RESTRICTIONS_BATTERY, true) + ) + } + + DisposableEffect(Unit) { + onDispose { onUpdateAutomatedCheck() } + } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.pref_updates_restrictions_title)) }, + text = { + Column { + Text(stringResource(R.string.pref_updates_restrictions_desc)) + Spacer(Modifier.height(dimensionResource(R.dimen.margin_small))) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + metered = !metered + Preferences.putBoolean( + context, + PREFERENCES_UPDATES_RESTRICTIONS_METERED, + metered + ) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = metered, + onCheckedChange = { checked -> + metered = checked + Preferences.putBoolean( + context, + PREFERENCES_UPDATES_RESTRICTIONS_METERED, + checked + ) + } + ) + Text(stringResource(R.string.pref_updates_restrictions_metered)) + } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + idle = !idle + Preferences.putBoolean( + context, + PREFERENCES_UPDATES_RESTRICTIONS_IDLE, + idle + ) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = idle, + onCheckedChange = { checked -> + idle = checked + Preferences.putBoolean( + context, + PREFERENCES_UPDATES_RESTRICTIONS_IDLE, + checked + ) + } + ) + Text(stringResource(R.string.pref_updates_restrictions_idle)) + } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + battery = !battery + Preferences.putBoolean( + context, + PREFERENCES_UPDATES_RESTRICTIONS_BATTERY, + battery + ) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = battery, + onCheckedChange = { checked -> + battery = checked + Preferences.putBoolean( + context, + PREFERENCES_UPDATES_RESTRICTIONS_BATTERY, + checked + ) + } + ) + Text(stringResource(R.string.pref_updates_restrictions_battery)) + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(android.R.string.ok)) + } + } + ) +} + +@PreviewWrapper(ThemePreviewProvider::class) +@Preview +@Composable +private fun UpdatesPreferenceScreenPreview() { + ScreenContent() +} From 0c60df635f71874792fcfd7a0ec8c0241c34c3ff Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Tue, 19 May 2026 12:12:23 +0530 Subject: [PATCH 08/27] 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). --- .../store/compose/ui/commons/MoreSheet.kt | 202 ++++++++++++++++++ .../store/compose/ui/commons/NetworkSheet.kt | 96 +++++++++ .../store/compose/ui/sheets/AppMenuSheet.kt | 148 +++++++++++++ 3 files changed, 446 insertions(+) create mode 100644 app/src/main/java/com/aurora/store/compose/ui/commons/MoreSheet.kt create mode 100644 app/src/main/java/com/aurora/store/compose/ui/commons/NetworkSheet.kt create mode 100644 app/src/main/java/com/aurora/store/compose/ui/sheets/AppMenuSheet.kt 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()) + } +} From f2474cfb91a0d39bf96ba56308957aed857fc021 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Tue, 19 May 2026 12:12:42 +0530 Subject: [PATCH 09/27] compose: overhaul SplashScreen and add GoogleLoginScreen SplashScreen reacts to NetworkProvider and ditches the separate SplashButton class for inline buttons. Google sign-in moves into the dedicated GoogleLoginScreen reachable from the splash flow. --- .../compose/ui/accounts/GoogleLoginScreen.kt | 140 +++++++ .../store/compose/ui/splash/SplashScreen.kt | 364 ++++++++++++++++++ 2 files changed, 504 insertions(+) create mode 100644 app/src/main/java/com/aurora/store/compose/ui/accounts/GoogleLoginScreen.kt create mode 100644 app/src/main/java/com/aurora/store/compose/ui/splash/SplashScreen.kt diff --git a/app/src/main/java/com/aurora/store/compose/ui/accounts/GoogleLoginScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/accounts/GoogleLoginScreen.kt new file mode 100644 index 000000000..fb44474c6 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/accounts/GoogleLoginScreen.kt @@ -0,0 +1,140 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.accounts + +import android.os.Build +import android.webkit.CookieManager +import android.webkit.WebChromeClient +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.aurora.gplayapi.helpers.AuthHelper +import com.aurora.store.AuroraApp +import com.aurora.store.R +import com.aurora.store.compose.navigation.Destination +import com.aurora.store.data.event.AuthEvent +import com.aurora.store.data.model.AccountType +import com.aurora.store.data.providers.AccountProvider +import com.aurora.store.util.AC2DMUtil +import com.aurora.store.viewmodel.auth.AuthViewModel + +private const val EMBEDDED_SETUP_URL = "https://accounts.google.com/EmbeddedSetup" +private const val AUTH_TOKEN = "oauth_token" +private const val JS_PROFILE_EMAIL = + "(function() { return document.getElementById('profileIdentifier').innerHTML; })();" + +@Composable +fun GoogleLoginScreen( + onNavigateTo: (Destination) -> Unit, + viewModel: AuthViewModel = hiltViewModel() +) { + val context = LocalContext.current + var progress by remember { mutableFloatStateOf(0f) } + var isIndeterminate by remember { mutableStateOf(true) } + var isLoading by remember { mutableStateOf(true) } + + LaunchedEffect(Unit) { + AuroraApp.events.authEvent.collect { event -> + if (event is AuthEvent.GoogleLogin) { + if (event.success) { + AccountProvider.login( + context, + event.email, + event.token, + AuthHelper.Token.AAS, + AccountType.GOOGLE + ) + } else { + Toast.makeText(context, R.string.toast_aas_token_failed, Toast.LENGTH_LONG) + .show() + } + onNavigateTo(Destination.Splash) + } + } + } + + Column(modifier = Modifier.fillMaxSize()) { + if (isLoading) { + if (isIndeterminate) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } else { + LinearProgressIndicator( + progress = { progress }, + modifier = Modifier.fillMaxWidth() + ) + } + } + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { ctx -> + val cookieManager = CookieManager.getInstance() + WebView(ctx).apply { + cookieManager.removeAllCookies(null) + cookieManager.acceptThirdPartyCookies(this) + cookieManager.setAcceptThirdPartyCookies(this, true) + + webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + super.onProgressChanged(view, newProgress) + if (newProgress != 0) { + isIndeterminate = false + progress = newProgress / 100f + isLoading = newProgress < 100 + } else { + isIndeterminate = true + isLoading = true + } + } + } + + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView, url: String) { + val cookies = CookieManager.getInstance().getCookie(url) + if (cookies != null) { + val cookieMap = AC2DMUtil.parseCookieString(cookies) + if (cookieMap.isNotEmpty() && cookieMap[AUTH_TOKEN] != null) { + val oauthToken = cookieMap[AUTH_TOKEN] + view.evaluateJavascript(JS_PROFILE_EMAIL) { result -> + val email = result.replace("\"", "") + viewModel.buildAuthData(view.context, email, oauthToken) + } + } + } + } + } + + settings.apply { + allowContentAccess = true + domStorageEnabled = true + javaScriptEnabled = true + cacheMode = WebSettings.LOAD_DEFAULT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + safeBrowsingEnabled = false + } + } + + loadUrl(EMBEDDED_SETUP_URL) + } + } + ) + } +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/splash/SplashScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/splash/SplashScreen.kt new file mode 100644 index 000000000..06cec48fd --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/splash/SplashScreen.kt @@ -0,0 +1,364 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.splash + +import android.accounts.Account +import android.accounts.AccountManager +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Base64 +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +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.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE +import com.aurora.gplayapi.helpers.AuthHelper +import com.aurora.store.BuildConfig +import com.aurora.store.R +import com.aurora.store.compose.navigation.Destination +import com.aurora.store.data.model.AuthState +import com.aurora.store.data.model.NetworkStatus +import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT +import com.aurora.store.util.PackageUtil +import com.aurora.store.util.Preferences +import com.aurora.store.viewmodel.auth.AuthViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SplashScreen( + viewModel: AuthViewModel = hiltViewModel(), + deepLinkPackageName: String? = null, + deepLinkDevId: String? = null, + onNavigateTo: (Destination) -> Unit = {} +) { + val context = LocalContext.current + val activity = LocalActivity.current as? ComponentActivity + + val authState by viewModel.authState.collectAsStateWithLifecycle() + val networkStatus by viewModel.networkProvider.status.collectAsStateWithLifecycle( + initialValue = NetworkStatus.AVAILABLE + ) + val isOnline = networkStatus == NetworkStatus.AVAILABLE + + val canMicroGLogin = PackageUtil.hasSupportedMicroGVariant(context) && + Preferences.getBoolean(context, Preferences.PREFERENCE_MICROG_AUTH, true) + + var anonymousLoading by remember { mutableStateOf(false) } + var googleLoading by remember { mutableStateOf(false) } + + val accountLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + val accountName = result.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) + if (!accountName.isNullOrBlank()) { + requestAuthTokenForGoogle( + viewModel = viewModel, + context = context, + accountName = accountName, + oldToken = null, + activity = activity, + onError = { googleLoading = false } + ) + } else { + googleLoading = false + } + } + + LaunchedEffect(authState) { + when (val state = authState) { + AuthState.Valid, AuthState.SignedIn -> { + anonymousLoading = false + googleLoading = false + when { + !deepLinkDevId.isNullOrBlank() -> onNavigateTo( + Destination.DevProfile(deepLinkDevId) + ) + !deepLinkPackageName.isNullOrBlank() -> onNavigateTo( + Destination.AppDetails(deepLinkPackageName) + ) + else -> onNavigateTo( + Destination.Main( + Preferences.getInteger( + context, + Preferences.PREFERENCE_DEFAULT_SELECTED_TAB + ) + ) + ) + } + } + is AuthState.PendingAccountManager -> requestAuthTokenForGoogle( + viewModel = viewModel, + context = context, + accountName = state.email, + oldToken = state.token, + activity = activity, + onError = { googleLoading = false } + ) + is AuthState.Failed -> { + anonymousLoading = false + googleLoading = false + } + else -> Unit + } + } + + LaunchedEffect(isOnline) { + if (isOnline && authState is AuthState.Failed) { + viewModel.retry() + } + } + + val isLoading = authState == AuthState.Init || + authState == AuthState.Available || + authState == AuthState.Fetching || + authState == AuthState.Verifying || + authState is AuthState.PendingAccountManager + + val showLoginButtons = authState is AuthState.Unavailable || + authState is AuthState.Failed || + authState is AuthState.SignedOut + + Scaffold( + topBar = { + TopAppBar( + title = {}, + actions = { + IconButton(onClick = { onNavigateTo(Destination.Settings) }) { + Icon( + painter = painterResource(R.drawable.ic_menu_settings), + contentDescription = stringResource(R.string.title_settings) + ) + } + IconButton(onClick = { onNavigateTo(Destination.About) }) { + Icon( + painter = painterResource(R.drawable.ic_menu_about), + contentDescription = stringResource(R.string.title_about) + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent, + scrolledContainerColor = Color.Transparent + ) + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .navigationBarsPadding() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(0.50f), + contentAlignment = Alignment.BottomCenter + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(R.drawable.ic_logo_alt), + contentDescription = null, + modifier = Modifier.size(dimensionResource(R.dimen.icon_size_splash)), + contentScale = ContentScale.FillBounds + ) + Text( + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = BuildConfig.VERSION_NAME, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .weight(0.1f) + .padding(horizontal = dimensionResource(R.dimen.padding_xlarge)), + contentAlignment = Alignment.Center + ) { + val (statusText, statusColor) = when { + !isOnline && showLoginButtons -> stringResource(R.string.title_no_network) to + MaterialTheme.colorScheme.error + authState is AuthState.Failed -> (authState as AuthState.Failed).status to + MaterialTheme.colorScheme.error + else -> statusText(authState) to MaterialTheme.colorScheme.onSurfaceVariant + } + Text( + text = statusText, + style = MaterialTheme.typography.bodyMedium, + color = statusColor, + textAlign = TextAlign.Center + ) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .weight(0.40f), + contentAlignment = Alignment.TopCenter + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp + ) + } else if (showLoginButtons) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy( + dimensionResource(R.dimen.margin_small) + ) + ) { + Button( + modifier = Modifier.width(dimensionResource(R.dimen.width_button)), + enabled = !anonymousLoading && !googleLoading && isOnline, + onClick = { + if (canMicroGLogin) { + googleLoading = true + accountLauncher.launch( + AccountManager.newChooseAccountIntent( + null, + null, + arrayOf(GOOGLE_ACCOUNT_TYPE), + null, + null, + null, + null + ) + ) + } else { + onNavigateTo(Destination.GoogleLogin) + } + } + ) { + Text(stringResource(R.string.account_google)) + } + if (BuildConfig.SHOW_ANONYMOUS_LOGIN) { + OutlinedButton( + modifier = Modifier.width(dimensionResource(R.dimen.width_button)), + enabled = !googleLoading && !anonymousLoading && isOnline, + onClick = { + anonymousLoading = true + viewModel.buildAnonymousAuthData() + } + ) { + Text(stringResource(R.string.account_anonymous)) + } + } + if (googleLoading || anonymousLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp + ) + } + } + } + } + } + } +} + +@Composable +private fun statusText(authState: AuthState): String = when (authState) { + AuthState.Init -> stringResource(R.string.session_init) + AuthState.Available -> stringResource(R.string.session_verifying) + AuthState.Fetching -> stringResource(R.string.requesting_new_session) + AuthState.Verifying -> stringResource(R.string.verifying_new_session) + AuthState.Valid -> stringResource(R.string.session_verifying) + AuthState.SignedIn -> stringResource(R.string.session_good) + AuthState.Unavailable, AuthState.SignedOut -> stringResource(R.string.session_login) + is AuthState.PendingAccountManager -> stringResource(R.string.requesting_new_session) + is AuthState.Failed -> authState.status +} + +private fun requestAuthTokenForGoogle( + viewModel: AuthViewModel, + context: android.content.Context, + accountName: String, + oldToken: String?, + activity: ComponentActivity?, + onError: () -> Unit +) { + try { + val accountManager = AccountManager.get(context) + if (oldToken != null) { + accountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, oldToken) + } + accountManager.getAuthToken( + Account(accountName, GOOGLE_ACCOUNT_TYPE), + GOOGLE_PLAY_AUTH_TOKEN_TYPE, + Bundle().apply { + putString("overridePackage", PACKAGE_NAME_PLAY_STORE) + putByteArray("overrideCertificate", Base64.decode(GOOGLE_PLAY_CERT, Base64.DEFAULT)) + }, + activity, + { result -> + val token = result.result.getString(AccountManager.KEY_AUTHTOKEN) + if (token != null) { + viewModel.buildGoogleAuthData(accountName, token, AuthHelper.Token.AUTH) + } else { + Log.e("SplashScreen", "AccountManager returned null auth token") + onError() + } + }, + Handler(Looper.getMainLooper()) + ) + } catch (_: Exception) { + Log.e("SplashScreen", "Failed to get authToken for Google login") + onError() + } +} From 0270dc6f392eb305b8583c6ceeb04e02ec38542a Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Tue, 19 May 2026 12:13:20 +0530 Subject: [PATCH 10/27] compose: migrate remaining screens and fix category browse CategoryBrowseScreen and ExpandedStreamBrowseScreen are added; the details / dev / dispenser / downloads / favourite / installed / search / spoof / blacklist / accounts / about screens, StreamBrowse, the onboarding MicroG page, the AppDetailsScreen plus the affected viewmodels (AuthViewModel, ExpandedStreamBrowse, CategoryStream, TopChart) are reworked to feed the new Compose entry points. Also fixes CategoryBrowse pagination and the cluster-header navigation regressions surfaced during migration. --- .../store/compose/ui/about/AboutScreen.kt | 8 +- .../compose/ui/accounts/AccountsScreen.kt | 19 +-- .../compose/ui/blacklist/BlacklistScreen.kt | 11 +- .../ui/commons/CategoryBrowseScreen.kt | 65 +++++++++ .../ui/commons/ExpandedStreamBrowseScreen.kt | 98 ++++++++++++++ .../ui/commons/PermissionRationaleScreen.kt | 6 +- .../compose/ui/commons/StreamBrowseScreen.kt | 23 ++-- .../compose/ui/details/AppDetailsScreen.kt | 57 ++++---- .../store/compose/ui/details/ExodusScreen.kt | 13 +- .../ui/details/ManualDownloadScreen.kt | 11 +- .../store/compose/ui/details/MicroGScreen.kt | 12 +- .../store/compose/ui/details/MoreScreen.kt | 24 ++-- .../compose/ui/details/PermissionScreen.kt | 8 +- .../store/compose/ui/details/ReviewScreen.kt | 8 +- .../compose/ui/details/ScreenshotScreen.kt | 10 +- .../store/compose/ui/dev/DevProfileScreen.kt | 24 ++-- .../compose/ui/dispenser/DispenserScreen.kt | 7 +- .../compose/ui/downloads/DownloadsScreen.kt | 19 ++- .../compose/ui/favourite/FavouriteScreen.kt | 17 +-- .../compose/ui/installed/InstalledScreen.kt | 28 ++-- .../store/compose/ui/onboarding/MicroGPage.kt | 2 - .../store/compose/ui/search/SearchScreen.kt | 28 ++-- .../store/compose/ui/spoof/SpoofScreen.kt | 16 +-- .../data/providers/PermissionProvider.kt | 5 + .../com/aurora/store/util/NotificationUtil.kt | 58 ++++---- .../store/viewmodel/auth/AuthViewModel.kt | 4 + .../browse/ExpandedStreamBrowseViewModel.kt | 125 +++++++++--------- .../subcategory/CategoryStreamViewModel.kt | 118 ++++++++--------- .../viewmodel/topchart/TopChartViewModel.kt | 26 +++- 29 files changed, 478 insertions(+), 372 deletions(-) create mode 100644 app/src/main/java/com/aurora/store/compose/ui/commons/CategoryBrowseScreen.kt create mode 100644 app/src/main/java/com/aurora/store/compose/ui/commons/ExpandedStreamBrowseScreen.kt diff --git a/app/src/main/java/com/aurora/store/compose/ui/about/AboutScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/about/AboutScreen.kt index 79936b56b..e5cc278a3 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/about/AboutScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/about/AboutScreen.kt @@ -46,7 +46,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.data.model.Link @Composable -fun AboutScreen(onNavigateUp: () -> Unit) { +fun AboutScreen() { var shouldShowAboutDialog by rememberSaveable { mutableStateOf(false) } if (shouldShowAboutDialog) { @@ -54,13 +54,12 @@ fun AboutScreen(onNavigateUp: () -> Unit) { } ScreenContent( - onNavigateUp = onNavigateUp, onAboutAurora = { shouldShowAboutDialog = true } ) } @Composable -private fun ScreenContent(onNavigateUp: () -> Unit = {}, onAboutAurora: () -> Unit = {}) { +private fun ScreenContent(onAboutAurora: () -> Unit = {}) { val context = LocalContext.current val linkURLS = stringArrayResource(R.array.link_urls) @@ -94,8 +93,7 @@ private fun ScreenContent(onNavigateUp: () -> Unit = {}, onAboutAurora: () -> Un Scaffold( topBar = { TopAppBar( - title = stringResource(R.string.title_about), - onNavigateUp = onNavigateUp + title = stringResource(R.string.title_about) ) } ) { paddingValues -> diff --git a/app/src/main/java/com/aurora/store/compose/ui/accounts/AccountsScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/accounts/AccountsScreen.kt index b8bb41717..1f0c7de50 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/accounts/AccountsScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/accounts/AccountsScreen.kt @@ -48,19 +48,18 @@ import com.aurora.Constants.URL_TOS import com.aurora.extensions.browse import com.aurora.store.R import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.data.providers.AccountProvider import com.aurora.store.viewmodel.accounts.AccountsViewModel @Composable fun AccountsScreen( - onNavigateUp: () -> Unit, - onNavigateToSplash: () -> Unit, + onNavigateTo: (Destination) -> Unit, viewModel: AccountsViewModel = hiltViewModel() ) { val userProfile = viewModel.authProvider.authData?.userProfile val avatar = when { - viewModel.authProvider.isAnonymous -> R.mipmap.ic_launcher userProfile != null -> userProfile.artwork.url else -> R.mipmap.ic_launcher } @@ -70,7 +69,6 @@ fun AccountsScreen( else -> stringResource(R.string.account_anonymous) } val email = when { - viewModel.authProvider.isAnonymous -> stringResource(R.string.account_anonymous_email) userProfile != null -> userProfile.email else -> stringResource(R.string.account_anonymous_email) } @@ -83,14 +81,13 @@ fun AccountsScreen( onConfirm = { shouldShowLogoutDialog = false AccountProvider.logout(context) - onNavigateToSplash() + onNavigateTo(Destination.Splash) }, onDismiss = { shouldShowLogoutDialog = false } ) } ScreenContent( - onNavigateUp = onNavigateUp, avatar = avatar, name = name, email = email, @@ -100,17 +97,15 @@ fun AccountsScreen( @Composable private fun ScreenContent( - avatar: Any = R.mipmap.ic_launcher, + avatar: Any = R.drawable.ic_account, name: String = stringResource(R.string.account_anonymous), email: String = stringResource(R.string.account_anonymous_email), - onNavigateUp: () -> Unit = {}, onLogout: () -> Unit = {} ) { Scaffold( topBar = { TopAppBar( - title = stringResource(R.string.title_account_manager), - onNavigateUp = onNavigateUp + title = stringResource(R.string.title_account_manager) ) } ) { paddingValues -> @@ -118,7 +113,7 @@ private fun ScreenContent( modifier = Modifier .padding(paddingValues) .fillMaxSize() - .padding(vertical = dimensionResource(R.dimen.padding_medium)), + .padding(vertical = dimensionResource(R.dimen.padding_small)), verticalArrangement = Arrangement.SpaceBetween ) { AssistHeader() @@ -126,7 +121,7 @@ private fun ScreenContent( Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium)) + verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small)) ) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) diff --git a/app/src/main/java/com/aurora/store/compose/ui/blacklist/BlacklistScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/blacklist/BlacklistScreen.kt index f82cb0e0b..fd71e7272 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/blacklist/BlacklistScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/blacklist/BlacklistScreen.kt @@ -7,6 +7,8 @@ package com.aurora.store.compose.ui.blacklist import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable @@ -51,7 +53,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewWrapper -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.aurora.Constants @@ -72,13 +73,12 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @Composable -fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hiltViewModel()) { +fun BlacklistScreen(viewModel: BlacklistViewModel = hiltViewModel()) { val context = LocalContext.current val packages by viewModel.filteredPackages.collectAsStateWithLifecycle() ScreenContent( packages = packages, - onNavigateUp = onNavigateUp, isPackageBlacklisted = { pkgName -> pkgName in viewModel.blacklist }, onBlacklistImport = { uri -> viewModel.importBlacklist(uri) @@ -99,7 +99,6 @@ fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hi @Composable private fun ScreenContent( packages: List? = null, - onNavigateUp: () -> Unit = {}, isPackageBlacklisted: (packageName: String) -> Boolean = { false }, onBlacklistImport: (uri: Uri) -> Unit = {}, onBlacklistExport: (uri: Uri) -> Unit = {}, @@ -109,6 +108,7 @@ private fun ScreenContent( onWhitelistAll: () -> Unit = {}, onSearch: (query: String) -> Unit = {} ) { + val activity = LocalActivity.current as? ComponentActivity val context = LocalContext.current val textFieldState = rememberTextFieldState() val searchBarState = rememberSearchBarState() @@ -224,7 +224,7 @@ private fun ScreenContent( state = searchBarState, inputField = inputField, navigationIcon = { - IconButton(onClick = onNavigateUp) { + IconButton(onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }) { Icon( painter = painterResource(R.drawable.ic_arrow_back), contentDescription = stringResource(R.string.action_back) @@ -333,7 +333,6 @@ private fun ScreenContent( } ScrollHint( listState = listState, - bottomPadding = 5.dp, modifier = Modifier.align(Alignment.BottomCenter) ) } diff --git a/app/src/main/java/com/aurora/store/compose/ui/commons/CategoryBrowseScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/commons/CategoryBrowseScreen.kt new file mode 100644 index 000000000..036b19556 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/commons/CategoryBrowseScreen.kt @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.commons + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.aurora.gplayapi.data.models.StreamBundle +import com.aurora.store.R +import com.aurora.store.compose.composable.Error +import com.aurora.store.compose.composable.StreamCarousel +import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.composition.collectForced +import com.aurora.store.compose.navigation.Destination +import com.aurora.store.data.model.ViewState +import com.aurora.store.viewmodel.subcategory.CategoryStreamViewModel + +@Composable +fun CategoryBrowseScreen( + title: String, + browseUrl: String, + onNavigateTo: (Destination) -> Unit, + viewModel: CategoryStreamViewModel = hiltViewModel( + creationCallback = { factory: CategoryStreamViewModel.Factory -> + factory.create(browseUrl) + } + ) +) { + val uiState by viewModel.viewState.collectForced(ViewState.Loading) + + Scaffold( + topBar = { TopAppBar(title = title) } + ) { paddingValues -> + if (uiState is ViewState.Error) { + Error( + modifier = Modifier.padding(paddingValues), + painter = painterResource(R.drawable.ic_disclaimer), + message = stringResource(R.string.error) + ) + } else { + val bundle = (uiState as? ViewState.Success<*>)?.data as? StreamBundle + StreamCarousel( + modifier = Modifier.padding(paddingValues), + streamBundle = bundle, + filterSingleAppClusters = false, + onHeaderClick = { cluster -> + if (cluster.clusterBrowseUrl.isNotBlank()) { + onNavigateTo(Destination.StreamBrowse(cluster)) + } + }, + onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) }, + onClusterScrolled = { viewModel.fetchNextCluster(it) }, + onScrolledToEnd = { viewModel.fetchNextPage() } + ) + } + } +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/commons/ExpandedStreamBrowseScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/commons/ExpandedStreamBrowseScreen.kt new file mode 100644 index 000000000..cd696d810 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/commons/ExpandedStreamBrowseScreen.kt @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.commons + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +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 androidx.paging.LoadState +import androidx.paging.compose.collectAsLazyPagingItems +import com.aurora.store.R +import com.aurora.store.compose.composable.ContainedLoadingIndicator +import com.aurora.store.compose.composable.Error +import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.composable.app.LargeAppListItem +import com.aurora.store.compose.navigation.Destination +import com.aurora.store.viewmodel.browse.ExpandedStreamBrowseViewModel +import kotlin.uuid.Uuid + +@Composable +fun ExpandedStreamBrowseScreen( + browseUrl: String, + defaultTitle: String, + onNavigateTo: (Destination) -> Unit, + viewModel: ExpandedStreamBrowseViewModel = hiltViewModel( + creationCallback = { factory: ExpandedStreamBrowseViewModel.Factory -> + factory.create(browseUrl) + } + ) +) { + val title by viewModel.title.collectAsStateWithLifecycle() + val apps = viewModel.apps.collectAsLazyPagingItems() + + Scaffold( + topBar = { + TopAppBar( + title = title.ifBlank { defaultTitle } + ) + } + ) { paddingValues -> + when (apps.loadState.refresh) { + is LoadState.Loading -> ContainedLoadingIndicator() + + is LoadState.Error -> { + Error( + modifier = Modifier.padding(paddingValues), + painter = painterResource(R.drawable.ic_disclaimer), + message = stringResource(R.string.error) + ) + } + + else -> { + if (apps.itemCount == 0) { + Error( + modifier = Modifier.padding(paddingValues), + painter = painterResource(R.drawable.ic_disclaimer), + message = stringResource(R.string.no_apps_available) + ) + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + verticalArrangement = Arrangement.spacedBy( + dimensionResource(R.dimen.margin_medium) + ) + ) { + items( + count = apps.itemCount, + key = { Uuid.random().toString() } + ) { index -> + apps[index]?.let { app -> + LargeAppListItem( + app = app, + onClick = { + onNavigateTo(Destination.AppDetails(app.packageName)) + } + ) + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/commons/PermissionRationaleScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/commons/PermissionRationaleScreen.kt index 1a27bb5c6..3aad2fc3d 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/commons/PermissionRationaleScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/commons/PermissionRationaleScreen.kt @@ -35,14 +35,12 @@ import kotlin.random.Random @Composable fun PermissionRationaleScreen( requiredPermissions: Set = emptySet(), - onNavigateUp: () -> Unit, onPermissionCallback: (type: PermissionType) -> Unit = {}, viewModel: PermissionRationaleViewModel = hiltViewModel() ) { val permissions by viewModel.permissions.collectAsStateWithLifecycle() ScreenContent( - onNavigateUp = onNavigateUp, permissions = permissions .filter { it.type in requiredPermissions } .map { permission -> permission.copy(optional = false) }, @@ -56,7 +54,6 @@ fun PermissionRationaleScreen( @Composable private fun ScreenContent( permissions: List = emptyList(), - onNavigateUp: () -> Unit = {}, onPermissionCallback: (type: PermissionType) -> Unit = {}, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2() ) { @@ -64,8 +61,7 @@ private fun ScreenContent( topBar = { TopAppBar( title = pluralStringResource(R.plurals.permissions_required, permissions.size), - navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, - onNavigateUp = onNavigateUp + navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon ) } ) { paddingValues -> diff --git a/app/src/main/java/com/aurora/store/compose/ui/commons/StreamBrowseScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/commons/StreamBrowseScreen.kt index 34c2e6f8b..a3571e00c 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/commons/StreamBrowseScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/commons/StreamBrowseScreen.kt @@ -5,12 +5,14 @@ package com.aurora.store.compose.ui.commons +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -29,6 +31,7 @@ import com.aurora.store.compose.composable.ContainedLoadingIndicator import com.aurora.store.compose.composable.Error import com.aurora.store.compose.composable.TopAppBar import com.aurora.store.compose.composable.app.LargeAppListItem +import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.viewmodel.browse.StreamBrowseViewModel @@ -39,8 +42,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @Composable fun StreamBrowseScreen( streamCluster: StreamCluster, - onNavigateUp: () -> Unit, - onNavigateToAppDetails: (packageName: String) -> Unit, + onNavigateTo: (Destination) -> Unit, viewModel: StreamBrowseViewModel = hiltViewModel( creationCallback = { factory: StreamBrowseViewModel.Factory -> factory.create(streamCluster) @@ -52,8 +54,7 @@ fun StreamBrowseScreen( ScreenContent( title = streamCluster.clusterTitle, apps = apps, - onNavigateUp = onNavigateUp, - onNavigateToAppDetails = onNavigateToAppDetails + onNavigateTo = onNavigateTo ) } @@ -61,12 +62,11 @@ fun StreamBrowseScreen( private fun ScreenContent( title: String = String(), apps: LazyPagingItems = emptyPagingItems(), - onNavigateToAppDetails: (packageName: String) -> Unit = {}, - onNavigateUp: () -> Unit = {} + onNavigateTo: (Destination) -> Unit = {} ) { Scaffold( topBar = { - TopAppBar(title = title, onNavigateUp = onNavigateUp) + TopAppBar(title = title) } ) { paddingValues -> when (apps.loadState.refresh) { @@ -91,7 +91,10 @@ private fun ScreenContent( LazyColumn( modifier = Modifier .fillMaxSize() - .padding(paddingValues) + .padding(paddingValues), + verticalArrangement = Arrangement.spacedBy( + dimensionResource(R.dimen.margin_medium) + ) ) { items( count = apps.itemCount, @@ -100,7 +103,9 @@ private fun ScreenContent( apps[index]?.let { app -> LargeAppListItem( app = app, - onClick = { onNavigateToAppDetails(app.packageName) } + onClick = { + onNavigateTo(Destination.AppDetails(app.packageName)) + } ) } } diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt index a4b9342fb..f36834149 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/details/AppDetailsScreen.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.tooling.preview.PreviewWrapper -import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -72,6 +71,7 @@ import com.aurora.store.compose.composable.Header import com.aurora.store.compose.composable.ScrollHint import com.aurora.store.compose.composable.TopAppBar import com.aurora.store.compose.composable.app.LargeAppListItem +import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.navigation.Screen import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider @@ -110,8 +110,7 @@ import kotlinx.coroutines.launch @Composable fun AppDetailsScreen( packageName: String, - onNavigateUp: () -> Unit, - onNavigateToAppDetails: (packageName: String) -> Unit, + onNavigateTo: (Destination) -> Unit, viewModel: AppDetailsViewModel = hiltViewModel(key = packageName), forceSinglePane: Boolean = false ) { @@ -136,11 +135,10 @@ fun AppDetailsScreen( ) { currentState -> when { currentState is AppState.Loading || app == null -> - ScreenContentLoading(onNavigateUp = onNavigateUp) + ScreenContentLoading() currentState is AppState.Error -> ScreenContentError( - onNavigateUp = onNavigateUp, message = currentState.message ) @@ -156,8 +154,7 @@ fun AppDetailsScreen( plexusScores = plexusScores, dataSafetyReport = dataSafetyReport, exodusReport = exodusReport, - onNavigateUp = onNavigateUp, - onNavigateToAppDetails = onNavigateToAppDetails, + onNavigateTo = onNavigateTo, onDownload = { requestedApp -> viewModel.enqueueDownload(requestedApp) }, onFavorite = { viewModel.toggleFavourite(loadedApp) }, onCancelDownload = { viewModel.cancelDownload(loadedApp) }, @@ -196,9 +193,9 @@ private fun stateKey(state: AppState, app: App?): String = when { * Composable to show progress while fetching app details */ @Composable -private fun ScreenContentLoading(onNavigateUp: () -> Unit = {}) { +private fun ScreenContentLoading() { Scaffold( - topBar = { TopAppBar(onNavigateUp = onNavigateUp) } + topBar = { TopAppBar() } ) { paddingValues -> ContainedLoadingIndicator(modifier = Modifier.padding(paddingValues)) } @@ -208,9 +205,9 @@ private fun ScreenContentLoading(onNavigateUp: () -> Unit = {}) { * Composable to display errors related to fetching app details */ @Composable -private fun ScreenContentError(onNavigateUp: () -> Unit = {}, message: String? = null) { +private fun ScreenContentError(message: String? = null) { Scaffold( - topBar = { TopAppBar(onNavigateUp = onNavigateUp) } + topBar = { TopAppBar() } ) { paddingValues -> Error( modifier = Modifier.padding(paddingValues), @@ -234,8 +231,7 @@ private fun ScreenContentApp( plexusScores: Scores? = null, dataSafetyReport: DataSafetyReport? = null, exodusReport: Report? = null, - onNavigateUp: () -> Unit = {}, - onNavigateToAppDetails: (packageName: String) -> Unit = {}, + onNavigateTo: (Destination) -> Unit = {}, onDownload: (requestedApp: App) -> Unit = {}, onFavorite: () -> Unit = {}, onCancelDownload: () -> Unit = {}, @@ -412,7 +408,6 @@ private fun ScreenContentApp( Scaffold( topBar = { TopAppBar( - onNavigateUp = onNavigateUp, actions = { if (shouldShowMenuOnMainPane) SetupMenu() } ) } @@ -535,7 +530,6 @@ private fun ScreenContentApp( } ScrollHint( listState = listState, - bottomPadding = 5.dp, modifier = Modifier.align(Alignment.BottomCenter) ) } @@ -546,7 +540,10 @@ private fun ScreenContentApp( fun SupportingPane() { Scaffold( topBar = { - TopAppBar(actions = { if (!shouldShowMenuOnMainPane) SetupMenu() }) + TopAppBar( + showNavigationIcon = false, + actions = { if (!shouldShowMenuOnMainPane) SetupMenu() } + ) } ) { paddingValues -> if (suggestions.isNotEmpty()) { @@ -568,12 +565,15 @@ private fun ScreenContentApp( LazyColumn( modifier = Modifier .fillMaxSize() - .padding(vertical = dimensionResource(R.dimen.padding_medium)) + .padding(vertical = dimensionResource(R.dimen.padding_medium)), + verticalArrangement = Arrangement.spacedBy( + dimensionResource(R.dimen.margin_medium) + ) ) { items(items = suggestions, key = { item -> item.id }) { app -> LargeAppListItem( app = app, - onClick = { onNavigateToAppDetails(app.packageName) } + onClick = { onNavigateTo(Destination.AppDetails(app.packageName)) } ) } } @@ -585,52 +585,43 @@ private fun ScreenContentApp( @Composable fun ExtraPane(screen: NavKey) = when (screen) { is ExtraScreen.Review -> ReviewScreen( - packageName = app.packageName, - onNavigateUp = ::onNavigateBack + packageName = app.packageName ) is ExtraScreen.Exodus -> ExodusScreen( - packageName = app.packageName, - onNavigateUp = ::onNavigateBack + packageName = app.packageName ) is ExtraScreen.More -> MoreScreen( packageName = app.packageName, - onNavigateUp = ::onNavigateBack, - onNavigateToAppDetails = onNavigateToAppDetails + onNavigateTo = onNavigateTo ) is ExtraScreen.Permission -> PermissionScreen( - packageName = app.packageName, - onNavigateUp = ::onNavigateBack + packageName = app.packageName ) is ExtraScreen.Screenshot -> ScreenshotScreen( packageName = app.packageName, - index = screen.index, - onNavigateUp = ::onNavigateBack + index = screen.index ) is ExtraScreen.ManualDownload -> ManualDownloadScreen( packageName = app.packageName, - onNavigateUp = ::onNavigateBack, onRequestInstall = { requestedApp -> onInstall(requestedApp) } ) is ExtraScreen.MicroG -> MicroGScreen( packageName = app.packageName, - onNavigateUp = ::onNavigateBack, onProceed = { onInstall(ignoreMicroG = true) } ) is Screen.DevProfile -> DevProfileScreen( publisherId = app.developerName, - onNavigateUp = ::onNavigateBack, - onNavigateToAppDetails = { onNavigateToAppDetails(it) } + onNavigateTo = onNavigateTo ) is Screen.PermissionRationale -> PermissionRationaleScreen( - onNavigateUp = ::onNavigateBack, requiredPermissions = screen.requiredPermissions, onPermissionCallback = { type -> val isStoragePermission = type == PermissionType.EXTERNAL_STORAGE || diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/ExodusScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/details/ExodusScreen.kt index f85046245..d6602fbce 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/details/ExodusScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/details/ExodusScreen.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewWrapper -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.aurora.Constants.EXODUS_REPORT_URL @@ -56,7 +55,6 @@ import com.aurora.store.viewmodel.details.ExodusViewModel @Composable fun ExodusScreen( packageName: String, - onNavigateUp: () -> Unit, appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName), exodusViewModel: ExodusViewModel = hiltViewModel(key = "$packageName/exodus"), windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2() @@ -80,7 +78,6 @@ fun ExodusScreen( null -> { ScreenContentError( topAppBarTitle = topAppBarTitle, - onNavigateUp = onNavigateUp, onRequestAnalysis = { context.browse("${EXODUS_SUBMIT_PAGE}${app!!.packageName}") } ) } @@ -90,7 +87,6 @@ fun ExodusScreen( topAppBarTitle = topAppBarTitle, report = report, trackers = trackers, - onNavigateUp = onNavigateUp, onRequestAnalysis = { context.browse("${EXODUS_SUBMIT_PAGE}${app!!.packageName}") } ) } @@ -102,7 +98,6 @@ private fun ScreenContentReport( topAppBarTitle: String? = null, report: Report? = null, trackers: List = emptyList(), - onNavigateUp: () -> Unit = {}, onRequestAnalysis: () -> Unit = {}, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2() ) { @@ -112,8 +107,7 @@ private fun ScreenContentReport( topBar = { TopAppBar( title = topAppBarTitle, - navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, - onNavigateUp = onNavigateUp + navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon ) }, floatingActionButton = { @@ -161,7 +155,6 @@ private fun ScreenContentReport( } ScrollHint( listState = listState, - bottomPadding = 5.dp, modifier = Modifier.align(Alignment.BottomCenter) ) } @@ -174,7 +167,6 @@ private fun ScreenContentReport( @Composable private fun ScreenContentError( topAppBarTitle: String? = null, - onNavigateUp: () -> Unit = {}, onRequestAnalysis: () -> Unit = {}, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2() ) { @@ -182,8 +174,7 @@ private fun ScreenContentError( topBar = { TopAppBar( title = topAppBarTitle, - navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, - onNavigateUp = onNavigateUp + navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon ) } ) { paddingValues -> diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/ManualDownloadScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/details/ManualDownloadScreen.kt index 81384ac1e..4ebd59b64 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/details/ManualDownloadScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/details/ManualDownloadScreen.kt @@ -6,6 +6,8 @@ package com.aurora.store.compose.ui.details +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -69,7 +71,6 @@ import kotlinx.coroutines.launch @Composable fun ManualDownloadScreen( packageName: String, - onNavigateUp: () -> Unit, onRequestInstall: (requestedApp: App) -> Unit, viewModel: AppDetailsViewModel = hiltViewModel(key = packageName), windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2() @@ -85,7 +86,6 @@ fun ManualDownloadScreen( state = state, topAppBarTitle = topAppBarTitle, currentVersionCode = app!!.versionCode, - onNavigateUp = onNavigateUp, onRequestInstall = { versionCode -> val requestedApp = app!!.copy( versionCode = versionCode, @@ -105,10 +105,10 @@ private fun ScreenContent( state: AppState = AppState.Unavailable, topAppBarTitle: String? = null, currentVersionCode: Long = 0L, - onNavigateUp: () -> Unit = {}, onRequestInstall: (versionCode: Long) -> Unit = {}, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2() ) { + val activity = LocalActivity.current as? ComponentActivity val coroutineScope = rememberCoroutineScope() val snackBarHostState = remember { SnackbarHostState() } val errorMessage = stringResource(R.string.manual_download_version_error) @@ -130,8 +130,7 @@ private fun ScreenContent( topBar = { TopAppBar( title = topAppBarTitle, - navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, - onNavigateUp = onNavigateUp + navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon ) }, snackbarHost = { SnackbarHost(hostState = snackBarHostState) } @@ -187,7 +186,7 @@ private fun ScreenContent( ) { FilledTonalButton( modifier = Modifier.weight(1F), - onClick = onNavigateUp + onClick = { activity?.onBackPressedDispatcher?.onBackPressed() } ) { Text( text = stringResource(R.string.action_close), diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/MicroGScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/details/MicroGScreen.kt index 145c6cda9..bb5c7010e 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/details/MicroGScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/details/MicroGScreen.kt @@ -6,6 +6,8 @@ package com.aurora.store.compose.ui.details +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -55,7 +57,6 @@ import com.aurora.store.viewmodel.onboarding.MicroGViewModel @Composable fun MicroGScreen( packageName: String, - onNavigateUp: () -> Unit, onProceed: () -> Unit, appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName), viewModel: MicroGViewModel = hiltViewModel(), @@ -70,7 +71,6 @@ fun MicroGScreen( ScreenContent( topAppBarTitle = topAppBarTitle, uiState = viewModel.uiState, - onNavigateUp = onNavigateUp, onInstall = { viewModel.downloadMicroG() }, onRetry = { viewModel.retryDownload() }, onProceed = onProceed @@ -81,12 +81,12 @@ fun MicroGScreen( private fun ScreenContent( topAppBarTitle: String? = null, uiState: MicroGUIState = MicroGUIState(), - onNavigateUp: () -> Unit = {}, onInstall: () -> Unit = {}, onRetry: () -> Unit = {}, onProceed: () -> Unit = {}, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2() ) { + val activity = LocalActivity.current as? ComponentActivity val context = LocalContext.current val listState = rememberLazyListState() @@ -94,8 +94,7 @@ private fun ScreenContent( topBar = { TopAppBar( title = topAppBarTitle, - navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, - onNavigateUp = onNavigateUp + navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon ) }, bottomBar = { @@ -112,7 +111,7 @@ private fun ScreenContent( ) { FilledTonalButton( modifier = Modifier.weight(1F), - onClick = onNavigateUp + onClick = { activity?.onBackPressedDispatcher?.onBackPressed() } ) { Text( text = stringResource(R.string.action_cancel), @@ -174,7 +173,6 @@ private fun ScreenContent( ScrollHint( listState = listState, - bottomPadding = 5.dp, modifier = Modifier.align(Alignment.BottomCenter) ) } diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/MoreScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/details/MoreScreen.kt index f9a66ccaa..5212c40db 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/details/MoreScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/details/MoreScreen.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.text.fromHtml import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewWrapper -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.aurora.extensions.adaptiveNavigationIcon @@ -44,6 +43,7 @@ import com.aurora.store.compose.composable.Info import com.aurora.store.compose.composable.ScrollHint import com.aurora.store.compose.composable.TopAppBar import com.aurora.store.compose.composable.app.AppListItem +import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.viewmodel.details.AppDetailsViewModel @@ -52,8 +52,7 @@ import com.aurora.store.viewmodel.details.MoreViewModel @Composable fun MoreScreen( packageName: String, - onNavigateUp: () -> Unit, - onNavigateToAppDetails: (packageName: String) -> Unit, + onNavigateTo: (Destination) -> Unit, appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName), moreViewModel: MoreViewModel = hiltViewModel( key = "$packageName/more", @@ -68,8 +67,7 @@ fun MoreScreen( ScreenContent( app = app!!, dependencies = dependencies, - onNavigateUp = onNavigateUp, - onNavigateToAppDetails = onNavigateToAppDetails + onNavigateTo = onNavigateTo ) } @@ -77,8 +75,7 @@ fun MoreScreen( private fun ScreenContent( app: App, dependencies: List? = null, - onNavigateUp: () -> Unit = {}, - onNavigateToAppDetails: (packageName: String) -> Unit = {}, + onNavigateTo: (Destination) -> Unit = {}, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2() ) { val topAppBarTitle = when { @@ -90,8 +87,7 @@ private fun ScreenContent( topBar = { TopAppBar( title = topAppBarTitle, - navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, - onNavigateUp = onNavigateUp + navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon ) } ) { paddingValues -> @@ -126,7 +122,7 @@ private fun ScreenContent( if (dependencies != null) { AppDependencies( dependencies = dependencies, - onNavigateToAppDetails = onNavigateToAppDetails + onNavigateTo = onNavigateTo ) } } @@ -137,7 +133,6 @@ private fun ScreenContent( } ScrollHint( listState = listState, - bottomPadding = 5.dp, modifier = Modifier.align(Alignment.BottomCenter) ) } @@ -148,10 +143,7 @@ private fun ScreenContent( * Composable to show dependencies of an app */ @Composable -private fun AppDependencies( - dependencies: List, - onNavigateToAppDetails: (packageName: String) -> Unit -) { +private fun AppDependencies(dependencies: List, onNavigateTo: (Destination) -> Unit) { Header(title = stringResource(R.string.details_dependencies)) if (dependencies.isEmpty()) { Info( @@ -162,7 +154,7 @@ private fun AppDependencies( items(items = dependencies, key = { item -> item.id }) { app -> AppListItem( app = app, - onClick = { onNavigateToAppDetails(app.packageName) } + onClick = { onNavigateTo(Destination.AppDetails(app.packageName)) } ) } } diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/PermissionScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/details/PermissionScreen.kt index d3d52194f..1fc1e911d 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/details/PermissionScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/details/PermissionScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewWrapper -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.aurora.extensions.adaptiveNavigationIcon @@ -47,7 +46,6 @@ import com.aurora.store.viewmodel.details.PermissionViewModel @Composable fun PermissionScreen( packageName: String, - onNavigateUp: () -> Unit, appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName), permissionViewModel: PermissionViewModel = hiltViewModel( key = "$packageName/permission", @@ -67,7 +65,6 @@ fun PermissionScreen( ScreenContent( topAppBarTitle = topAppBarTitle, - onNavigateUp = onNavigateUp, permissionsInfo = permissionsInfo ) } @@ -76,7 +73,6 @@ fun PermissionScreen( private fun ScreenContent( topAppBarTitle: String? = null, permissionsInfo: Map = emptyMap(), - onNavigateUp: () -> Unit = {}, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2() ) { val packageManager = LocalContext.current.packageManager @@ -85,8 +81,7 @@ private fun ScreenContent( topBar = { TopAppBar( title = topAppBarTitle, - navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, - onNavigateUp = onNavigateUp + navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon ) } ) { paddingValues -> @@ -127,7 +122,6 @@ private fun ScreenContent( } ScrollHint( listState = listState, - bottomPadding = 5.dp, modifier = Modifier.align(Alignment.BottomCenter) ) } diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/ReviewScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/details/ReviewScreen.kt index 79b295803..f0fd4b088 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/details/ReviewScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/details/ReviewScreen.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewWrapper -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState @@ -64,7 +63,6 @@ import kotlinx.coroutines.flow.MutableStateFlow @Composable fun ReviewScreen( packageName: String, - onNavigateUp: () -> Unit, appDetailsViewModel: AppDetailsViewModel = hiltViewModel(key = packageName), reviewViewModel: ReviewViewModel = hiltViewModel( key = "$packageName/review", @@ -85,7 +83,6 @@ fun ReviewScreen( ScreenContent( topAppBarTitle = topAppBarTitle, reviews = reviews, - onNavigateUp = onNavigateUp, onFilter = { filter -> reviewViewModel.fetchReviews(filter) } ) } @@ -93,7 +90,6 @@ fun ReviewScreen( @Composable private fun ScreenContent( topAppBarTitle: String? = null, - onNavigateUp: () -> Unit = {}, reviews: LazyPagingItems = emptyPagingItems(), onFilter: (filter: Review.Filter) -> Unit = {}, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2() @@ -103,8 +99,7 @@ private fun ScreenContent( Column { TopAppBar( title = topAppBarTitle, - navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, - onNavigateUp = onNavigateUp + navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon ) FilterHeader { filter -> onFilter(filter) } } @@ -142,7 +137,6 @@ private fun ScreenContent( } ScrollHint( listState = listState, - bottomPadding = 5.dp, modifier = Modifier.align(Alignment.BottomCenter) ) } diff --git a/app/src/main/java/com/aurora/store/compose/ui/details/ScreenshotScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/details/ScreenshotScreen.kt index ab808a78e..444b76e94 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/details/ScreenshotScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/details/ScreenshotScreen.kt @@ -17,7 +17,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewWrapper @@ -36,7 +36,6 @@ import com.aurora.store.viewmodel.details.AppDetailsViewModel fun ScreenshotScreen( packageName: String, index: Int, - onNavigateUp: () -> Unit, viewModel: AppDetailsViewModel = hiltViewModel(key = packageName), windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2() ) { @@ -49,7 +48,6 @@ fun ScreenshotScreen( ScreenContent( topAppBarTitle = topAppBarTitle, - onNavigateUp = onNavigateUp, screenshots = app!!.screenshots, index = index ) @@ -58,12 +56,11 @@ fun ScreenshotScreen( @Composable private fun ScreenContent( topAppBarTitle: String? = null, - onNavigateUp: () -> Unit = {}, screenshots: List = emptyList(), index: Int = 0, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2() ) { - val displayMetrics = LocalContext.current.resources.displayMetrics + val displayMetrics = LocalResources.current.displayMetrics val pagerState = rememberPagerState(initialPage = index) { screenshots.size } LaunchedEffect(key1 = index) { @@ -74,8 +71,7 @@ private fun ScreenContent( topBar = { TopAppBar( title = topAppBarTitle, - navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, - onNavigateUp = onNavigateUp + navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon ) } ) { paddingValues -> diff --git a/app/src/main/java/com/aurora/store/compose/ui/dev/DevProfileScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/dev/DevProfileScreen.kt index 054f55c8f..aa2b3ac2d 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/dev/DevProfileScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/dev/DevProfileScreen.kt @@ -6,6 +6,7 @@ package com.aurora.store.compose.ui.dev +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -34,6 +35,7 @@ import com.aurora.store.compose.composable.ContainedLoadingIndicator import com.aurora.store.compose.composable.Error import com.aurora.store.compose.composable.TopAppBar import com.aurora.store.compose.composable.app.LargeAppListItem +import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.viewmodel.details.DevProfileViewModel @@ -48,8 +50,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @Composable fun DevProfileScreen( developerId: String, - onNavigateUp: () -> Unit, - onNavigateToAppDetails: (packageName: String) -> Unit, + onNavigateTo: (Destination) -> Unit, viewModel: DevProfileViewModel = hiltViewModel() ) { // TODO: Implement when migrating logic for current DevProfileFragment @@ -62,8 +63,7 @@ fun DevProfileScreen( @Composable fun DevProfileScreen( publisherId: String, - onNavigateUp: () -> Unit, - onNavigateToAppDetails: (packageName: String) -> Unit, + onNavigateTo: (Destination) -> Unit, viewModel: SearchViewModel = hiltViewModel() ) { val apps = viewModel.apps.collectAsLazyPagingItems() @@ -73,8 +73,7 @@ fun DevProfileScreen( ScreenContent( apps = apps, topAppBarTitle = publisherId, - onNavigateUp = onNavigateUp, - onNavigateToAppDetails = onNavigateToAppDetails + onNavigateTo = onNavigateTo ) } @@ -82,16 +81,14 @@ fun DevProfileScreen( private fun ScreenContent( apps: LazyPagingItems, topAppBarTitle: String, - onNavigateUp: () -> Unit = {}, - onNavigateToAppDetails: (packageName: String) -> Unit = {}, + onNavigateTo: (Destination) -> Unit = {}, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfoV2() ) { Scaffold( topBar = { TopAppBar( title = topAppBarTitle, - navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon, - onNavigateUp = onNavigateUp + navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon ) } ) { paddingValues -> @@ -111,13 +108,16 @@ private fun ScreenContent( modifier = Modifier .padding(paddingValues) .fillMaxSize() - .padding(vertical = dimensionResource(R.dimen.padding_medium)) + .padding(vertical = dimensionResource(R.dimen.padding_medium)), + verticalArrangement = Arrangement.spacedBy( + dimensionResource(R.dimen.margin_medium) + ) ) { items(count = apps.itemCount, key = apps.itemKey { it.id }) { index -> apps[index]?.let { app -> LargeAppListItem( app = app, - onClick = { onNavigateToAppDetails(app.packageName) } + onClick = { onNavigateTo(Destination.AppDetails(app.packageName)) } ) } } diff --git a/app/src/main/java/com/aurora/store/compose/ui/dispenser/DispenserScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/dispenser/DispenserScreen.kt index 25645ad44..e5121bc7c 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/dispenser/DispenserScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/dispenser/DispenserScreen.kt @@ -35,7 +35,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.viewmodel.dispenser.DispenserViewModel @Composable -fun DispenserScreen(onNavigateUp: () -> Unit, viewModel: DispenserViewModel = hiltViewModel()) { +fun DispenserScreen(viewModel: DispenserViewModel = hiltViewModel()) { val dispensers by viewModel.dispensers.collectAsStateWithLifecycle() var shouldShowInputDialog by rememberSaveable { mutableStateOf(false) } @@ -63,7 +63,6 @@ fun DispenserScreen(onNavigateUp: () -> Unit, viewModel: DispenserViewModel = hi ScreenContent( dispensers = dispensers, - onNavigateUp = onNavigateUp, onAddDispenser = { shouldShowInputDialog = true }, onRemoveDispenser = { url -> shouldRemoveDispenser = url } ) @@ -71,7 +70,6 @@ fun DispenserScreen(onNavigateUp: () -> Unit, viewModel: DispenserViewModel = hi @Composable private fun ScreenContent( - onNavigateUp: () -> Unit = {}, dispensers: Set = emptySet(), onAddDispenser: () -> Unit = {}, onRemoveDispenser: (url: String) -> Unit = {} @@ -81,8 +79,7 @@ private fun ScreenContent( Scaffold( topBar = { TopAppBar( - title = stringResource(R.string.pref_dispenser_title), - onNavigateUp = onNavigateUp + title = stringResource(R.string.pref_dispenser_title) ) }, floatingActionButton = { diff --git a/app/src/main/java/com/aurora/store/compose/ui/downloads/DownloadsScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/downloads/DownloadsScreen.kt index 068c5fe09..500059878 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/downloads/DownloadsScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/downloads/DownloadsScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewWrapper -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.paging.LoadState import androidx.paging.PagingData @@ -45,6 +44,7 @@ import com.aurora.store.compose.composable.DownloadListItem import com.aurora.store.compose.composable.Error import com.aurora.store.compose.composable.ScrollHint import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.ui.downloads.menu.DownloadsMenu @@ -57,9 +57,8 @@ import kotlinx.coroutines.flow.MutableStateFlow @Composable fun DownloadsScreen( - onNavigateUp: () -> Unit, - viewModel: DownloadsViewModel = hiltViewModel(), - onNavigateToAppDetails: (packageName: String) -> Unit + onNavigateTo: (Destination) -> Unit, + viewModel: DownloadsViewModel = hiltViewModel() ) { val context = LocalContext.current val downloads = viewModel.downloads.collectAsLazyPagingItems() @@ -79,9 +78,8 @@ fun DownloadsScreen( ) ScreenContent( - onNavigateUp = onNavigateUp, downloads = downloads, - onNavigateToAppDetails = onNavigateToAppDetails, + onNavigateTo = onNavigateTo, onCancelAll = { viewModel.cancelAll() }, onForceClearAll = { viewModel.clearAll() }, onClearFinished = { viewModel.clearFinished() }, @@ -100,8 +98,7 @@ fun DownloadsScreen( @Composable private fun ScreenContent( downloads: LazyPagingItems = emptyPagingItems(), - onNavigateUp: () -> Unit = {}, - onNavigateToAppDetails: (packageName: String) -> Unit = {}, + onNavigateTo: (Destination) -> Unit = {}, onCancel: (packageName: String) -> Unit = {}, onClear: (download: Download) -> Unit = {}, onExport: (download: Download) -> Unit = {}, @@ -133,7 +130,6 @@ private fun ScreenContent( topBar = { TopAppBar( title = stringResource(R.string.title_download_manager), - onNavigateUp = onNavigateUp, actions = { if (downloads.itemCount != 0) SetupMenu() } ) } @@ -173,7 +169,9 @@ private fun ScreenContent( modifier = Modifier.animateItem(), download = download, onClick = { - onNavigateToAppDetails(download.packageName) + onNavigateTo( + Destination.AppDetails(download.packageName) + ) }, onClear = { onClear(download) }, onCancel = { onCancel(download.packageName) }, @@ -185,7 +183,6 @@ private fun ScreenContent( } ScrollHint( listState = listState, - bottomPadding = 5.dp, modifier = Modifier.align(Alignment.BottomCenter) ) } diff --git a/app/src/main/java/com/aurora/store/compose/ui/favourite/FavouriteScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/favourite/FavouriteScreen.kt index 75306c346..546693ed8 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/favourite/FavouriteScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/favourite/FavouriteScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewWrapper -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.paging.LoadState import androidx.paging.PagingData @@ -45,6 +44,7 @@ import com.aurora.store.compose.composable.Error import com.aurora.store.compose.composable.FavouriteListItem import com.aurora.store.compose.composable.ScrollHint import com.aurora.store.compose.composable.TopAppBar +import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.preview.FavouritePreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.ui.favourite.menu.FavouriteMenu @@ -57,8 +57,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @Composable fun FavouriteScreen( - onNavigateUp: () -> Unit, - onNavigateToAppDetails: (packageName: String) -> Unit, + onNavigateTo: (Destination) -> Unit, viewModel: FavouriteViewModel = hiltViewModel() ) { val context = LocalContext.current @@ -90,8 +89,7 @@ fun FavouriteScreen( ScreenContent( favourites = favourites, - onNavigateUp = onNavigateUp, - onNavigateToAppDetails = onNavigateToAppDetails, + onNavigateTo = onNavigateTo, onRemoveFavourite = { packageName -> viewModel.removeFavourite(packageName) }, onImportFavourites = { documentOpenLauncher.launch(arrayOf(JSON_MIME_TYPE)) @@ -107,8 +105,7 @@ fun FavouriteScreen( @Composable private fun ScreenContent( favourites: LazyPagingItems = emptyPagingItems(), - onNavigateUp: () -> Unit = {}, - onNavigateToAppDetails: (packageName: String) -> Unit = {}, + onNavigateTo: (Destination) -> Unit = {}, onRemoveFavourite: (packageName: String) -> Unit = {}, onImportFavourites: () -> Unit = {}, onExportFavourites: () -> Unit = {} @@ -135,7 +132,6 @@ private fun ScreenContent( topBar = { TopAppBar( title = stringResource(R.string.title_favourites_manager), - onNavigateUp = onNavigateUp, actions = { SetupMenu() } ) } @@ -175,7 +171,9 @@ private fun ScreenContent( modifier = Modifier.animateItem(), favourite = favourite, onClick = { - onNavigateToAppDetails(favourite.packageName) + onNavigateTo( + Destination.AppDetails(favourite.packageName) + ) }, onClear = { onRemoveFavourite(favourite.packageName) } ) @@ -184,7 +182,6 @@ private fun ScreenContent( } ScrollHint( listState = listState, - bottomPadding = 5.dp, modifier = Modifier.align(Alignment.BottomCenter) ) } diff --git a/app/src/main/java/com/aurora/store/compose/ui/installed/InstalledScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/installed/InstalledScreen.kt index 84e8396e3..ed7d9f6f3 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/installed/InstalledScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/installed/InstalledScreen.kt @@ -6,6 +6,7 @@ package com.aurora.store.compose.ui.installed +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -20,12 +21,12 @@ 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.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewWrapper -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.paging.LoadState import androidx.paging.PagingData @@ -40,6 +41,7 @@ import com.aurora.store.compose.composable.Error import com.aurora.store.compose.composable.ScrollHint import com.aurora.store.compose.composable.TopAppBar import com.aurora.store.compose.composable.app.LargeAppListItem +import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.viewmodel.all.InstalledViewModel @@ -48,24 +50,21 @@ import kotlinx.coroutines.flow.MutableStateFlow @Composable fun InstalledScreen( - onNavigateUp: () -> Unit, - onNavigateToAppDetails: (packageName: String) -> Unit, + onNavigateTo: (Destination) -> Unit, viewModel: InstalledViewModel = hiltViewModel() ) { val apps = viewModel.apps.collectAsLazyPagingItems() ScreenContent( apps = apps, - onNavigateUp = onNavigateUp, - onNavigateToAppDetails = onNavigateToAppDetails + onNavigateTo = onNavigateTo ) } @Composable private fun ScreenContent( - onNavigateUp: () -> Unit = {}, apps: LazyPagingItems = emptyPagingItems(), - onNavigateToAppDetails: (packageName: String) -> Unit = {} + onNavigateTo: (Destination) -> Unit = {} ) { /* * For some reason paging3 frequently out-of-nowhere invalidates the list which causes @@ -78,8 +77,7 @@ private fun ScreenContent( Scaffold( topBar = { TopAppBar( - title = stringResource(R.string.title_apps_games), - onNavigateUp = onNavigateUp + title = stringResource(R.string.title_apps_games) ) } ) { paddingValues -> @@ -106,7 +104,10 @@ private fun ScreenContent( val listState = rememberLazyListState() Box(modifier = Modifier.fillMaxSize()) { LazyColumn( - state = listState + state = listState, + verticalArrangement = Arrangement.spacedBy( + dimensionResource(R.dimen.margin_medium) + ) ) { items( count = apps.itemCount, @@ -115,14 +116,17 @@ private fun ScreenContent( apps[index]?.let { app -> LargeAppListItem( app = app, - onClick = { onNavigateToAppDetails(app.packageName) } + onClick = { + onNavigateTo( + Destination.AppDetails(app.packageName) + ) + } ) } } } ScrollHint( listState = listState, - bottomPadding = 5.dp, modifier = Modifier.align(Alignment.BottomCenter) ) } diff --git a/app/src/main/java/com/aurora/store/compose/ui/onboarding/MicroGPage.kt b/app/src/main/java/com/aurora/store/compose/ui/onboarding/MicroGPage.kt index b0ff6d245..9b763682b 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/onboarding/MicroGPage.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/onboarding/MicroGPage.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewWrapper -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.aurora.extensions.toast import com.aurora.store.R @@ -105,7 +104,6 @@ private fun ScreenContent( ScrollHint( listState = listState, - bottomPadding = 5.dp, modifier = Modifier.align(Alignment.BottomCenter) ) } diff --git a/app/src/main/java/com/aurora/store/compose/ui/search/SearchScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/search/SearchScreen.kt index b20cf151b..1a443f1ad 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/search/SearchScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/search/SearchScreen.kt @@ -6,8 +6,9 @@ package com.aurora.store.compose.ui.search +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity import androidx.annotation.StringRes -import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement @@ -62,7 +63,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.tooling.preview.PreviewWrapper -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState @@ -78,6 +78,7 @@ import com.aurora.store.compose.composable.Error import com.aurora.store.compose.composable.ScrollHint import com.aurora.store.compose.composable.SearchSuggestionListItem import com.aurora.store.compose.composable.app.LargeAppListItem +import com.aurora.store.compose.navigation.Destination import com.aurora.store.compose.preview.AppPreviewProvider import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.ui.details.AppDetailsScreen @@ -91,7 +92,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @Composable -fun SearchScreen(onNavigateUp: () -> Unit, viewModel: SearchViewModel = hiltViewModel()) { +fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) { val suggestions by viewModel.suggestions.collectAsStateWithLifecycle() val results = viewModel.apps.collectAsLazyPagingItems() @@ -102,7 +103,6 @@ fun SearchScreen(onNavigateUp: () -> Unit, viewModel: SearchViewModel = hiltView ScreenContent( suggestions = suggestions, results = results, - onNavigateUp = onNavigateUp, onSearch = onSearchCallback, onFetchSuggestions = onFetchSuggestionsCallback, onFilter = { filter -> viewModel.filterResults(filter) }, @@ -114,12 +114,12 @@ fun SearchScreen(onNavigateUp: () -> Unit, viewModel: SearchViewModel = hiltView private fun ScreenContent( suggestions: List = emptyList(), results: LazyPagingItems = emptyPagingItems(), - onNavigateUp: () -> Unit = {}, onFetchSuggestions: (String) -> Unit = {}, onSearch: (String) -> Unit = {}, onFilter: (filter: SearchFilter) -> Unit = {}, isAnonymous: Boolean = true ) { + val activity = LocalActivity.current as? ComponentActivity val textFieldState = rememberTextFieldState() val searchBarState = rememberSearchBarState() var isSearching by rememberSaveable { mutableStateOf(false) } @@ -186,7 +186,7 @@ private fun ScreenContent( ) }, leadingIcon = { - IconButton(onClick = onNavigateUp) { + IconButton(onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }) { Icon( painter = painterResource(R.drawable.ic_arrow_back), contentDescription = stringResource(R.string.action_back) @@ -231,10 +231,7 @@ private fun ScreenContent( @Composable fun ListPane() { - // TODO: https://issuetracker.google.com/issues/445720462 Scaffold( - modifier = Modifier - .focusable(), topBar = { SearchBar() } ) { paddingValues -> Column( @@ -272,7 +269,10 @@ private fun ScreenContent( Box(modifier = Modifier.fillMaxSize()) { LazyColumn( state = listState, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy( + dimensionResource(R.dimen.margin_medium) + ) ) { items( count = results.itemCount, @@ -288,7 +288,6 @@ private fun ScreenContent( } ScrollHint( listState = listState, - bottomPadding = 5.dp, modifier = Modifier.align(Alignment.BottomCenter) ) } @@ -306,9 +305,10 @@ private fun ScreenContent( this != null -> { AppDetailsScreen( packageName = this, - onNavigateToAppDetails = { packageName -> showDetailPane(packageName) }, - onNavigateUp = { - coroutineScope.launch { scaffoldNavigator.navigateBack() } + onNavigateTo = { destination -> + if (destination is Destination.AppDetails) { + showDetailPane(destination.packageName) + } }, forceSinglePane = true ) diff --git a/app/src/main/java/com/aurora/store/compose/ui/spoof/SpoofScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/spoof/SpoofScreen.kt index 4c2f1e392..f43b42fa2 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/spoof/SpoofScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/spoof/SpoofScreen.kt @@ -36,6 +36,7 @@ import com.aurora.Constants import com.aurora.extensions.toast 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.spoof.menu.MenuItem import com.aurora.store.compose.ui.spoof.menu.SpoofMenu import com.aurora.store.compose.ui.spoof.navigation.SpoofPage @@ -44,14 +45,9 @@ import com.aurora.store.viewmodel.spoof.SpoofViewModel import kotlinx.coroutines.launch @Composable -fun SpoofScreen( - onNavigateUp: () -> Unit, - onNavigateToSplash: () -> Unit, - viewModel: SpoofViewModel = hiltViewModel() -) { +fun SpoofScreen(onNavigateTo: (Destination) -> Unit, viewModel: SpoofViewModel = hiltViewModel()) { ScreenContent( - onNavigateUp = onNavigateUp, - onNavigateToSplash = onNavigateToSplash, + onNavigateTo = onNavigateTo, onDeviceSpoofImport = { uri -> viewModel.importDeviceSpoof(uri) }, onDeviceSpoofExport = { uri -> viewModel.exportDeviceSpoof(uri) } ) @@ -60,8 +56,7 @@ fun SpoofScreen( @Composable private fun ScreenContent( pages: List = listOf(SpoofPage.DEVICE, SpoofPage.LOCALE), - onNavigateUp: () -> Unit = {}, - onNavigateToSplash: () -> Unit = {}, + onNavigateTo: (Destination) -> Unit = {}, onDeviceSpoofImport: (uri: Uri) -> Unit = {}, onDeviceSpoofExport: (uri: Uri) -> Unit = {} ) { @@ -101,7 +96,7 @@ private fun ScreenContent( when (result) { SnackbarResult.ActionPerformed -> { AccountProvider.logout(context) - onNavigateToSplash() + onNavigateTo(Destination.Splash) } else -> Unit @@ -133,7 +128,6 @@ private fun ScreenContent( topBar = { TopAppBar( title = stringResource(R.string.title_spoof_manager), - onNavigateUp = onNavigateUp, actions = { SetupMenu() } ) } diff --git a/app/src/main/java/com/aurora/store/data/providers/PermissionProvider.kt b/app/src/main/java/com/aurora/store/data/providers/PermissionProvider.kt index 9226be283..7c169b34d 100644 --- a/app/src/main/java/com/aurora/store/data/providers/PermissionProvider.kt +++ b/app/src/main/java/com/aurora/store/data/providers/PermissionProvider.kt @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2026 Aurora OSS + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package com.aurora.store.data.providers import android.Manifest diff --git a/app/src/main/java/com/aurora/store/util/NotificationUtil.kt b/app/src/main/java/com/aurora/store/util/NotificationUtil.kt index da2e95e64..99a6b9634 100644 --- a/app/src/main/java/com/aurora/store/util/NotificationUtil.kt +++ b/app/src/main/java/com/aurora/store/util/NotificationUtil.kt @@ -10,14 +10,13 @@ import android.graphics.Bitmap import android.graphics.Color import android.net.Uri import android.os.Build -import android.os.Bundle import androidx.core.app.NotificationCompat import androidx.core.app.PendingIntentCompat import androidx.core.content.getSystemService -import androidx.navigation.NavDeepLinkBuilder import com.aurora.Constants -import com.aurora.store.MainActivity +import com.aurora.store.ComposeActivity import com.aurora.store.R +import com.aurora.store.compose.navigation.Screen import com.aurora.store.data.activity.InstallActivity import com.aurora.store.data.helper.DownloadHelper import com.aurora.store.data.installer.AppInstaller @@ -213,16 +212,7 @@ object NotificationUtil { .build() fun getUpdateNotification(context: Context, updatesList: List): Notification { - val arguments = Bundle().apply { - putInt("destinationId", R.id.updatesFragment) - } - - val contentIntent = NavDeepLinkBuilder(context) - .setGraph(R.navigation.mobile_navigation) - .setDestination(R.id.splashFragment) - .setComponentName(MainActivity::class.java) - .setArguments(arguments) - .createPendingIntent() + val contentIntent = getContentIntentForMain(context, initialTab = 2) return NotificationCompat.Builder(context, Constants.NOTIFICATION_CHANNEL_UPDATES) .setSmallIcon(R.drawable.ic_updates) @@ -297,7 +287,7 @@ object NotificationUtil { .setContentTitle(context.getString(R.string.authentication_required_title)) .setContentText(context.getString(R.string.authentication_required_unarchive)) .setAutoCancel(true) - .setContentIntent(getContentIntentForSplash(context, packageName)) + .setContentIntent(getContentIntentForDetails(context, packageName)) .setCategory(NotificationCompat.CATEGORY_ERROR) .setPriority(NotificationCompat.PRIORITY_HIGH) .build() @@ -359,28 +349,32 @@ object NotificationUtil { ) } - private fun getContentIntentForSplash(context: Context, packageName: String): PendingIntent { - val arguments = Bundle().apply { - putString("packageName", packageName) + private fun getContentIntentForDetails(context: Context, packageName: String): PendingIntent? { + val intent = Intent(context, ComposeActivity::class.java).apply { + putExtra(Screen.PARCEL_KEY, Screen.AppDetails(packageName) as android.os.Parcelable) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } - return NavDeepLinkBuilder(context) - .setGraph(R.navigation.mobile_navigation) - .setDestination(R.id.splashFragment) - .setComponentName(MainActivity::class.java) - .setArguments(arguments) - .createPendingIntent() + return PendingIntentCompat.getActivity( + context, + packageName.hashCode().absoluteValue, + intent, + PendingIntent.FLAG_UPDATE_CURRENT, + false + ) } - private fun getContentIntentForDetails(context: Context, packageName: String): PendingIntent { - val arguments = Bundle().apply { - putString("packageName", packageName) + private fun getContentIntentForMain(context: Context, initialTab: Int): PendingIntent? { + val intent = Intent(context, ComposeActivity::class.java).apply { + putExtra(Screen.PARCEL_KEY, Screen.Main(initialTab) as android.os.Parcelable) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } - return NavDeepLinkBuilder(context) - .setGraph(R.navigation.mobile_navigation) - .setDestination(R.id.splashFragment) - .setComponentName(MainActivity::class.java) - .setArguments(arguments) - .createPendingIntent() + return PendingIntentCompat.getActivity( + context, + initialTab, + intent, + PendingIntent.FLAG_UPDATE_CURRENT, + false + ) } private fun getInstallIntent(context: Context, download: Download): PendingIntent? { diff --git a/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt index 20a06c9b3..ed8ec0541 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt @@ -34,6 +34,7 @@ import com.aurora.store.data.model.AccountType import com.aurora.store.data.model.AuthState import com.aurora.store.data.providers.AccountProvider import com.aurora.store.data.providers.AuthProvider +import com.aurora.store.data.providers.NetworkProvider import com.aurora.store.util.AC2DMTask import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences @@ -50,6 +51,7 @@ import kotlinx.coroutines.launch @HiltViewModel class AuthViewModel @Inject constructor( val authProvider: AuthProvider, + val networkProvider: NetworkProvider, @ApplicationContext private val context: Context, private val aC2DMTask: AC2DMTask ) : ViewModel() { @@ -117,6 +119,8 @@ class AuthViewModel @Inject constructor( } } + fun retry() = updateAuthState() + private fun updateAuthState() { if (_authState.value != AuthState.Fetching) { if (AccountProvider.isLoggedIn(context)) { diff --git a/app/src/main/java/com/aurora/store/viewmodel/browse/ExpandedStreamBrowseViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/browse/ExpandedStreamBrowseViewModel.kt index 9679aa0e0..9c503fc0b 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/browse/ExpandedStreamBrowseViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/browse/ExpandedStreamBrowseViewModel.kt @@ -1,85 +1,90 @@ /* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * + * SPDX-FileCopyrightText: 2024 Aurora OSS + * SPDX-FileCopyrightText: 2021 Rahul Kumar Patel + * SPDX-License-Identifier: GPL-3.0-or-later */ package com.aurora.store.viewmodel.browse import android.util.Log -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn import com.aurora.extensions.TAG -import com.aurora.gplayapi.data.models.StreamCluster +import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.helpers.ExpandedBrowseHelper +import com.aurora.store.data.PageResult +import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach -@HiltViewModel -class ExpandedStreamBrowseViewModel @Inject constructor( +@HiltViewModel(assistedFactory = ExpandedStreamBrowseViewModel.Factory::class) +class ExpandedStreamBrowseViewModel @AssistedInject constructor( + @Assisted val browseUrl: String, private val streamHelper: ExpandedBrowseHelper ) : ViewModel() { - val liveData: MutableLiveData = MutableLiveData() - var streamCluster: StreamCluster = StreamCluster() - - fun getInitialCluster(expandedStreamUrl: String) { - viewModelScope.launch(Dispatchers.IO) { - supervisorScope { - try { - val browseResponse = streamHelper.getBrowseStreamResponse(expandedStreamUrl) - if (browseResponse.hasBrowseTab()) { - streamCluster = - streamHelper.getExpandedBrowseClusters(browseResponse.browseTab.listUrl) - liveData.postValue(streamCluster) - } else { - } - } catch (_: Exception) { - } - } - } + @AssistedFactory + interface Factory { + fun create(browseUrl: String): ExpandedStreamBrowseViewModel } - fun next() { - Log.e(TAG, "NEXT CALED") - viewModelScope.launch(Dispatchers.IO) { - supervisorScope { - try { - val newCluster = streamHelper.getExpandedBrowseClusters( - streamCluster.clusterNextPageUrl - ) + private val _title = MutableStateFlow("") + val title = _title.asStateFlow() - streamCluster = streamCluster.copy( - clusterAppList = streamCluster.clusterAppList + newCluster.clusterAppList, - clusterNextPageUrl = newCluster.clusterNextPageUrl - ) + private val _apps = MutableStateFlow>(PagingData.empty()) + val apps = _apps.asStateFlow() - liveData.postValue(streamCluster) + init { + fetchApps() + } - if (!streamCluster.hasNext()) { - Log.i(TAG, "End of Bundle") + private fun fetchApps() { + var listUrl = "" + var nextPageUrl = "" + + manualPager { page -> + val items = try { + when (page) { + 1 -> { + val browseResponse = streamHelper.getBrowseStreamResponse(browseUrl) + if (browseResponse.hasBrowseTab()) { + listUrl = browseResponse.browseTab.listUrl + val cluster = streamHelper.getExpandedBrowseClusters(listUrl) + _title.value = cluster.clusterTitle + nextPageUrl = cluster.clusterNextPageUrl + cluster.clusterAppList + } else { + emptyList() + } + } + + else -> { + if (nextPageUrl.isNotBlank()) { + val cluster = streamHelper.getExpandedBrowseClusters(nextPageUrl) + nextPageUrl = cluster.clusterNextPageUrl + cluster.clusterAppList + } else { + emptyList() + } } - } catch (exception: Exception) { - Log.e(TAG, "Failed to fetch next stream", exception) } + } catch (exception: Exception) { + Log.e(TAG, "Failed to fetch apps for page $page", exception) + emptyList() } - } + PageResult(items) + }.flow.distinctUntilChanged() + .cachedIn(viewModelScope) + .onEach { _apps.value = it } + .launchIn(viewModelScope) } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/subcategory/CategoryStreamViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/subcategory/CategoryStreamViewModel.kt index 5976baf93..05cba8221 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/subcategory/CategoryStreamViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/subcategory/CategoryStreamViewModel.kt @@ -1,26 +1,12 @@ /* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * + * SPDX-FileCopyrightText: 2024 Aurora OSS + * SPDX-FileCopyrightText: 2021 Rahul Kumar Patel + * SPDX-License-Identifier: GPL-3.0-or-later */ package com.aurora.store.viewmodel.subcategory import android.util.Log -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.extensions.TAG @@ -30,70 +16,82 @@ import com.aurora.gplayapi.helpers.contracts.CategoryStreamContract import com.aurora.gplayapi.helpers.contracts.StreamContract import com.aurora.gplayapi.helpers.web.WebCategoryStreamHelper import com.aurora.store.data.model.ViewState +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope -@HiltViewModel -class CategoryStreamViewModel @Inject constructor( +@HiltViewModel(assistedFactory = CategoryStreamViewModel.Factory::class) +class CategoryStreamViewModel @AssistedInject constructor( + @Assisted val browseUrl: String, private val webCategoryStreamHelper: WebCategoryStreamHelper ) : ViewModel() { - val liveData: MutableLiveData = MutableLiveData() + @AssistedFactory + interface Factory { + fun create(browseUrl: String): CategoryStreamViewModel + } - private var stash: MutableMap = mutableMapOf() + // SharedFlow (instead of StateFlow) because StreamBundle/StreamCluster override equals + // to compare only id, which is preserved by copy(). StateFlow would conflate every update + // and break paginated scroll loading. + private val _viewState = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val viewState: SharedFlow = _viewState.asSharedFlow() private val categoryStreamContract: CategoryStreamContract get() = webCategoryStreamHelper - fun getStreamBundle(browseUrl: String) { - liveData.postValue(ViewState.Loading) - observe(browseUrl) + private var streamBundle = StreamBundle() + + init { + fetchNextPage() } - fun observe(browseUrl: String) { - liveData.postValue(ViewState.Loading) + fun fetchNextPage() { viewModelScope.launch(Dispatchers.IO) { supervisorScope { - val bundle = targetBundle(browseUrl) - if (bundle.streamClusters.isNotEmpty()) { - liveData.postValue(ViewState.Success(stash)) + if (streamBundle.streamClusters.isNotEmpty()) { + _viewState.tryEmit(ViewState.Success(streamBundle)) } try { - if (!bundle.hasCluster() || bundle.hasNext()) { - // Fetch new stream bundle - val newBundle = if (bundle.streamClusters.isEmpty()) { + if (!streamBundle.hasCluster() || streamBundle.hasNext()) { + val newBundle = if (streamBundle.streamClusters.isEmpty()) { categoryStreamContract.fetch(browseUrl) } else { categoryStreamContract.nextStreamBundle( StreamContract.Category.NONE, - bundle.streamNextPageUrl + streamBundle.streamNextPageUrl ) } - // Update old bundle - val mergedBundle = bundle.copy( - streamClusters = bundle.streamClusters + newBundle.streamClusters, + streamBundle = streamBundle.copy( + streamClusters = streamBundle.streamClusters + newBundle.streamClusters, streamNextPageUrl = newBundle.streamNextPageUrl ) - stash[browseUrl] = mergedBundle - // Post updated to UI - liveData.postValue(ViewState.Success(stash)) + _viewState.tryEmit(ViewState.Success(streamBundle)) } else { Log.i(TAG, "End of Bundle") } } catch (e: Exception) { - liveData.postValue(ViewState.Error(e.message)) + _viewState.tryEmit(ViewState.Error(e.message)) } } } } - fun observeCluster(browseUrl: String, streamCluster: StreamCluster) { + fun fetchNextCluster(streamCluster: StreamCluster) { viewModelScope.launch(Dispatchers.IO) { supervisorScope { try { @@ -101,36 +99,24 @@ class CategoryStreamViewModel @Inject constructor( val newCluster = categoryStreamContract.nextStreamCluster( streamCluster.clusterNextPageUrl ) - updateCluster(browseUrl, streamCluster.id, newCluster) - liveData.postValue(ViewState.Success(stash)) + val mergedCluster = streamCluster.copy( + clusterNextPageUrl = newCluster.clusterNextPageUrl, + clusterAppList = + streamCluster.clusterAppList + newCluster.clusterAppList + ) + + val newClusters = streamBundle.streamClusters.toMutableMap().apply { + this[streamCluster.id] = mergedCluster + } + streamBundle = streamBundle.copy(streamClusters = newClusters) + _viewState.tryEmit(ViewState.Success(streamBundle)) } else { Log.i(TAG, "End of cluster") } } catch (e: Exception) { - liveData.postValue(ViewState.Error(e.message)) + Log.e(TAG, "Failed to fetch next cluster", e) } } } } - - private fun updateCluster(browseUrl: String, clusterID: Int, newCluster: StreamCluster) { - val bundle = targetBundle(browseUrl) - bundle.streamClusters[clusterID]?.let { oldCluster -> - val mergedCluster = oldCluster.copy( - clusterNextPageUrl = newCluster.clusterNextPageUrl, - clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList - ) - - val newStreamClusters = bundle.streamClusters.toMutableMap().apply { - this[clusterID] = mergedCluster - } - - stash.put(browseUrl, bundle.copy(streamClusters = newStreamClusters)) - } - } - - private fun targetBundle(browseUrl: String): StreamBundle { - val streamBundle = stash.getOrPut(browseUrl) { StreamBundle() } - return streamBundle - } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/topchart/TopChartViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/topchart/TopChartViewModel.kt index 4fe8f9f07..256b9fc75 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/topchart/TopChartViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/topchart/TopChartViewModel.kt @@ -19,7 +19,6 @@ package com.aurora.store.viewmodel.topchart -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.StreamCluster @@ -30,6 +29,10 @@ import com.aurora.store.data.model.ViewState import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope @@ -40,7 +43,14 @@ class TopChartViewModel @Inject constructor( private var stash: TopChartStash = mutableMapOf() - val liveData: MutableLiveData = MutableLiveData() + // SharedFlow (instead of StateFlow) because StreamCluster overrides equals to compare + // only id, which is preserved by copy(). StateFlow would conflate paginated updates and + // break scroll loading. See CategoryStreamViewModel for the same fix. + private val _state = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val state: SharedFlow = _state.asSharedFlow() private val topChartsContract: TopChartsContract get() = webTopChartsHelper @@ -48,14 +58,18 @@ class TopChartViewModel @Inject constructor( fun getStreamCluster(type: TopChartsContract.Type, chart: TopChartsContract.Chart) { viewModelScope.launch(Dispatchers.IO) { if (targetCluster(type, chart).clusterAppList.isNotEmpty()) { - liveData.postValue(ViewState.Success(stash)) + _state.tryEmit(ViewState.Success(targetCluster(type, chart))) + return@launch } + _state.tryEmit(ViewState.Loading) + try { val cluster = topChartsContract.getCluster(type.value, chart.value) updateCluster(type, chart, cluster) - liveData.postValue(ViewState.Success(stash)) - } catch (_: Exception) { + _state.tryEmit(ViewState.Success(targetCluster(type, chart))) + } catch (e: Exception) { + _state.tryEmit(ViewState.Error(e.message)) } } } @@ -72,7 +86,7 @@ class TopChartViewModel @Inject constructor( updateCluster(type, chart, newCluster) - liveData.postValue(ViewState.Success(stash)) + _state.tryEmit(ViewState.Success(targetCluster(type, chart))) } } catch (_: Exception) { } From fef75cb5b3d315a6d8ed0313637726e51c44ebfe Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Tue, 19 May 2026 12:13:42 +0530 Subject: [PATCH 11/27] legacy: remove Epoxy views and custom view layouts Drops the Epoxy carousel controllers, view models, shimmer placeholders, custom layouts (ActionHeader, StateButton, UpdateButton) and bespoke preference helpers, all now superseded by the Compose composables. --- .../view/custom/layouts/ActionHeaderLayout.kt | 82 ------- .../view/custom/layouts/button/StateButton.kt | 77 ------- .../custom/layouts/button/UpdateButton.kt | 81 ------- .../custom/preference/AuroraListPreference.kt | 50 ----- .../custom/preference/M3EditTextPreference.kt | 46 ---- .../custom/preference/M3ListPreference.kt | 40 ---- .../EndlessRecyclerOnScrollListener.kt | 173 --------------- .../controller/CategoryCarouselController.kt | 29 --- .../controller/GenericCarouselController.kt | 102 --------- .../view/epoxy/groups/CarouselHorizontal.kt | 35 --- .../view/epoxy/groups/CarouselModelGroup.kt | 105 --------- .../view/epoxy/groups/CarouselShimmerGroup.kt | 60 ----- .../store/view/epoxy/views/AppProgressView.kt | 32 --- .../store/view/epoxy/views/BaseModel.kt | 50 ----- .../aurora/store/view/epoxy/views/BaseView.kt | 51 ----- .../store/view/epoxy/views/CategoryView.kt | 51 ----- .../store/view/epoxy/views/HeaderView.kt | 62 ------ .../view/epoxy/views/UpdateHeaderView.kt | 59 ----- .../store/view/epoxy/views/app/AppListView.kt | 90 -------- .../view/epoxy/views/app/AppUpdateView.kt | 205 ------------------ .../store/view/epoxy/views/app/AppView.kt | 72 ------ .../store/view/epoxy/views/app/NoAppView.kt | 72 ------ .../epoxy/views/shimmer/AppListViewShimmer.kt | 37 ---- .../epoxy/views/shimmer/AppViewShimmer.kt | 37 ---- .../epoxy/views/shimmer/HeaderViewShimmer.kt | 37 ---- 25 files changed, 1735 deletions(-) delete mode 100644 app/src/main/java/com/aurora/store/view/custom/layouts/ActionHeaderLayout.kt delete mode 100644 app/src/main/java/com/aurora/store/view/custom/layouts/button/StateButton.kt delete mode 100644 app/src/main/java/com/aurora/store/view/custom/layouts/button/UpdateButton.kt delete mode 100644 app/src/main/java/com/aurora/store/view/custom/preference/AuroraListPreference.kt delete mode 100644 app/src/main/java/com/aurora/store/view/custom/preference/M3EditTextPreference.kt delete mode 100644 app/src/main/java/com/aurora/store/view/custom/preference/M3ListPreference.kt delete mode 100644 app/src/main/java/com/aurora/store/view/custom/recycler/EndlessRecyclerOnScrollListener.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/controller/CategoryCarouselController.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/controller/GenericCarouselController.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/groups/CarouselHorizontal.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/groups/CarouselModelGroup.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/groups/CarouselShimmerGroup.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/views/AppProgressView.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/views/BaseModel.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/views/BaseView.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/views/CategoryView.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/views/HeaderView.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/views/UpdateHeaderView.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/views/app/AppListView.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/views/app/AppUpdateView.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/views/app/AppView.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/views/app/NoAppView.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/views/shimmer/AppListViewShimmer.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/views/shimmer/AppViewShimmer.kt delete mode 100644 app/src/main/java/com/aurora/store/view/epoxy/views/shimmer/HeaderViewShimmer.kt diff --git a/app/src/main/java/com/aurora/store/view/custom/layouts/ActionHeaderLayout.kt b/app/src/main/java/com/aurora/store/view/custom/layouts/ActionHeaderLayout.kt deleted file mode 100644 index e43045fca..000000000 --- a/app/src/main/java/com/aurora/store/view/custom/layouts/ActionHeaderLayout.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.custom.layouts - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.widget.RelativeLayout -import com.aurora.store.R -import com.aurora.store.databinding.ViewActionHeaderBinding - -class ActionHeaderLayout : RelativeLayout { - - private lateinit var binding: ViewActionHeaderBinding - - constructor(context: Context) : super(context) { - init(context, null) - } - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - init(context, attrs) - } - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init(context, attrs) - } - - private fun init(context: Context, attrs: AttributeSet?) { - val view = inflate(context, R.layout.view_action_header, this) - binding = ViewActionHeaderBinding.bind(view) - - val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ActionHeaderLayout) - val textTitle = typedArray.getString(R.styleable.ActionHeaderLayout_headerTitle) - val textSubtitle = typedArray.getString(R.styleable.ActionHeaderLayout_headerSubtitle) - - typedArray.recycle() - - textTitle?.let { - binding.txtTitle.text = it - } - - textSubtitle?.let { - binding.txtSubtitle.visibility = View.VISIBLE - binding.txtSubtitle.text = it - } - } - - fun setTitle(header: String?) { - binding.txtTitle.text = header - } - - fun setSubTitle(subHeader: String?) { - binding.txtSubtitle.visibility = View.VISIBLE - binding.txtSubtitle.text = subHeader - } - - fun addClickListener(onclickListener: OnClickListener?) { - binding.imgAction.visibility = View.VISIBLE - binding.imgAction.setOnClickListener(onclickListener) - } -} diff --git a/app/src/main/java/com/aurora/store/view/custom/layouts/button/StateButton.kt b/app/src/main/java/com/aurora/store/view/custom/layouts/button/StateButton.kt deleted file mode 100644 index 1fce7497a..000000000 --- a/app/src/main/java/com/aurora/store/view/custom/layouts/button/StateButton.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.custom.layouts.button - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.widget.RelativeLayout -import androidx.core.content.ContextCompat -import com.aurora.store.R -import com.aurora.store.databinding.ViewStateButtonBinding - -class StateButton : RelativeLayout { - - private lateinit var binding: ViewStateButtonBinding - - constructor(context: Context) : super(context) { - init(context, null) - } - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - init(context, attrs) - } - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init(context, attrs) - } - - private fun init(context: Context, attrs: AttributeSet?) { - val view = inflate(context, R.layout.view_state_button, this) - binding = ViewStateButtonBinding.bind(view) - - val typedArray = context.obtainStyledAttributes(attrs, R.styleable.StateButton) - val btnTxt = typedArray.getString(R.styleable.StateButton_btnStateText) - val btnIcon = typedArray.getResourceId( - R.styleable.StateButton_btnStateIcon, - R.drawable.ic_arrow_right - ) - - binding.btn.text = btnTxt - binding.btn.icon = ContextCompat.getDrawable(context, btnIcon) - typedArray.recycle() - } - - fun updateProgress(isVisible: Boolean) { - if (isVisible) { - binding.progress.visibility = View.VISIBLE - } else { - binding.progress.visibility = View.INVISIBLE - } - } - - fun addOnClickListener(onClickListener: OnClickListener) { - binding.btn.setOnClickListener(onClickListener) - } -} diff --git a/app/src/main/java/com/aurora/store/view/custom/layouts/button/UpdateButton.kt b/app/src/main/java/com/aurora/store/view/custom/layouts/button/UpdateButton.kt deleted file mode 100644 index 54c195377..000000000 --- a/app/src/main/java/com/aurora/store/view/custom/layouts/button/UpdateButton.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.custom.layouts.button - -import android.content.Context -import android.util.AttributeSet -import android.widget.RelativeLayout -import com.aurora.extensions.runOnUiThread -import com.aurora.store.R -import com.aurora.store.data.model.DownloadStatus -import com.aurora.store.databinding.ViewUpdateButtonBinding - -class UpdateButton : RelativeLayout { - - private lateinit var binding: ViewUpdateButtonBinding - - constructor(context: Context) : super(context) { - init(context) - } - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - init(context) - } - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init(context) - } - - private fun init(context: Context) { - val view = inflate(context, R.layout.view_update_button, this) - binding = ViewUpdateButtonBinding.bind(view) - } - - fun updateState(downloadStatus: DownloadStatus) { - val displayChild = when (downloadStatus) { - DownloadStatus.QUEUED, - DownloadStatus.DOWNLOADING, - DownloadStatus.VERIFYING -> 1 - - else -> 0 - } - - if (binding.viewFlipper.displayedChild != displayChild) { - runOnUiThread { - binding.viewFlipper.displayedChild = displayChild - } - } - - // Not allowed to cancel installation at this point - binding.btnNegative.isEnabled = downloadStatus != DownloadStatus.VERIFYING - } - - fun addPositiveOnClickListener(onClickListener: OnClickListener?) { - binding.btnPositive.setOnClickListener(onClickListener) - } - - fun addNegativeOnClickListener(onClickListener: OnClickListener?) { - binding.btnNegative.setOnClickListener(onClickListener) - } -} diff --git a/app/src/main/java/com/aurora/store/view/custom/preference/AuroraListPreference.kt b/app/src/main/java/com/aurora/store/view/custom/preference/AuroraListPreference.kt deleted file mode 100644 index 58b249022..000000000 --- a/app/src/main/java/com/aurora/store/view/custom/preference/AuroraListPreference.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.custom.preference - -import android.content.Context -import android.util.AttributeSet -import androidx.preference.ListPreference - -class AuroraListPreference : ListPreference { - - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) - - constructor( - context: Context, - attrs: AttributeSet?, - defStyleAttr: Int, - defStyleRes: Int - ) : super(context, attrs, defStyleAttr, defStyleRes) - - override fun getPersistedString(defaultReturnValue: String?): String = - getPersistedInt(defaultReturnValue?.toInt() ?: -1).toString() - - override fun persistString(value: String?): Boolean = - if (value != null) persistInt(Integer.valueOf(value)) else false -} diff --git a/app/src/main/java/com/aurora/store/view/custom/preference/M3EditTextPreference.kt b/app/src/main/java/com/aurora/store/view/custom/preference/M3EditTextPreference.kt deleted file mode 100644 index 0e496b427..000000000 --- a/app/src/main/java/com/aurora/store/view/custom/preference/M3EditTextPreference.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.aurora.store.view.custom.preference - -import android.app.Dialog -import android.os.Bundle -import android.view.WindowManager -import androidx.preference.EditTextPreferenceDialogFragmentCompat -import com.google.android.material.dialog.MaterialAlertDialogBuilder - -class M3EditTextPreference : EditTextPreferenceDialogFragmentCompat() { - - companion object { - const val PREFERENCE_DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG" - - fun newInstance(key: String?): M3EditTextPreference { - val fragment = M3EditTextPreference() - val bundle = Bundle(1) - bundle.putString(ARG_KEY, key) - fragment.arguments = bundle - return fragment - } - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = MaterialAlertDialogBuilder(requireContext()) - .setTitle(preference.dialogTitle) - .setIcon(preference.dialogIcon) - .setPositiveButton(preference.positiveButtonText, this) - .setNegativeButton(preference.negativeButtonText, this) - - val contentView = onCreateDialogView(requireContext()) - if (contentView != null) { - onBindDialogView(contentView) - builder.setView(contentView) - } else { - builder.setMessage(preference.dialogMessage) - } - - onPrepareDialogBuilder(builder) - return builder.create() - } - - override fun onResume() { - super.onResume() - dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) - } -} diff --git a/app/src/main/java/com/aurora/store/view/custom/preference/M3ListPreference.kt b/app/src/main/java/com/aurora/store/view/custom/preference/M3ListPreference.kt deleted file mode 100644 index 92790b434..000000000 --- a/app/src/main/java/com/aurora/store/view/custom/preference/M3ListPreference.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.aurora.store.view.custom.preference - -import android.app.Dialog -import android.os.Bundle -import androidx.preference.ListPreferenceDialogFragmentCompat -import com.google.android.material.dialog.MaterialAlertDialogBuilder - -class M3ListPreference : ListPreferenceDialogFragmentCompat() { - - companion object { - const val PREFERENCE_DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG" - - fun newInstance(key: String?): M3ListPreference { - val fragment = M3ListPreference() - val bundle = Bundle(1) - bundle.putString(ARG_KEY, key) - fragment.arguments = bundle - return fragment - } - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = MaterialAlertDialogBuilder(requireContext()) - .setTitle(preference.dialogTitle) - .setIcon(preference.dialogIcon) - .setPositiveButton(preference.positiveButtonText, this) - .setNegativeButton(preference.negativeButtonText, this) - - val contentView = onCreateDialogView(requireContext()) - if (contentView != null) { - onBindDialogView(contentView) - builder.setView(contentView) - } else { - builder.setMessage(preference.dialogMessage) - } - - onPrepareDialogBuilder(builder) - return builder.create() - } -} diff --git a/app/src/main/java/com/aurora/store/view/custom/recycler/EndlessRecyclerOnScrollListener.kt b/app/src/main/java/com/aurora/store/view/custom/recycler/EndlessRecyclerOnScrollListener.kt deleted file mode 100644 index 5e0dea093..000000000 --- a/app/src/main/java/com/aurora/store/view/custom/recycler/EndlessRecyclerOnScrollListener.kt +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.custom.recycler - -import android.view.View -import androidx.recyclerview.widget.OrientationHelper -import androidx.recyclerview.widget.RecyclerView - -abstract class EndlessRecyclerOnScrollListener : RecyclerView.OnScrollListener { - - private var enabled = true - private var previousTotal = 0 - private var isLoading = true - private var visibleThreshold = RecyclerView.NO_POSITION - - var firstVisibleItem: Int = 0 - private set - var visibleItemCount: Int = 0 - private set - var totalItemCount: Int = 0 - private set - - private var isOrientationHelperVertical: Boolean = false - private var orientationHelper: OrientationHelper? = null - - var currentPage = 0 - private set - - lateinit var layoutManager: RecyclerView.LayoutManager - private set - - constructor() - - constructor(visibleThreshold: Int) { - this.visibleThreshold = visibleThreshold - } - - private fun findFirstVisibleItemPosition(recyclerView: RecyclerView): Int { - val child = findOneVisibleChild(0, layoutManager.childCount, true, false) - return if (child == null) { - RecyclerView.NO_POSITION - } else { - recyclerView.getChildAdapterPosition(child) - } - } - - private fun findLastVisibleItemPosition(recyclerView: RecyclerView): Int { - val child = findOneVisibleChild(recyclerView.childCount - 1, -1, false, true) - return if (child == null) { - RecyclerView.NO_POSITION - } else { - recyclerView.getChildAdapterPosition(child) - } - } - - private fun findOneVisibleChild( - fromIndex: Int, - toIndex: Int, - completelyVisible: Boolean, - acceptPartiallyVisible: Boolean - ): View? { - if (layoutManager.canScrollVertically() != isOrientationHelperVertical || - orientationHelper == null - ) { - isOrientationHelperVertical = layoutManager.canScrollVertically() - orientationHelper = if (isOrientationHelperVertical) { - OrientationHelper.createVerticalHelper(layoutManager) - } else { - OrientationHelper.createHorizontalHelper(layoutManager) - } - } - - val mOrientationHelper = this.orientationHelper ?: return null - - val start = mOrientationHelper.startAfterPadding - val end = mOrientationHelper.endAfterPadding - val next = if (toIndex > fromIndex) 1 else -1 - var partiallyVisible: View? = null - var i = fromIndex - while (i != toIndex) { - val child = layoutManager.getChildAt(i) - if (child != null) { - val childStart = mOrientationHelper.getDecoratedStart(child) - val childEnd = mOrientationHelper.getDecoratedEnd(child) - if (childStart < end && childEnd > start) { - if (completelyVisible) { - if (childStart >= start && childEnd <= end) { - return child - } else if (acceptPartiallyVisible && partiallyVisible == null) { - partiallyVisible = child - } - } else { - return child - } - } - } - i += next - } - return partiallyVisible - } - - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - - if (enabled) { - if (!::layoutManager.isInitialized) { - layoutManager = recyclerView.layoutManager - ?: throw RuntimeException("A LayoutManager is required") - } - - if (visibleThreshold == RecyclerView.NO_POSITION) { - visibleThreshold = findLastVisibleItemPosition(recyclerView) - - findFirstVisibleItemPosition(recyclerView) - } - - visibleItemCount = recyclerView.childCount - totalItemCount = layoutManager.itemCount - firstVisibleItem = findFirstVisibleItemPosition(recyclerView) - - if (isLoading) { - if (totalItemCount > previousTotal) { - isLoading = false - previousTotal = totalItemCount - } - } - - if (!isLoading && - totalItemCount - visibleItemCount <= firstVisibleItem + visibleThreshold - ) { - currentPage++ - onLoadMore(currentPage) - isLoading = true - } - } - } - - fun enable(): EndlessRecyclerOnScrollListener { - enabled = true - return this - } - - fun disable(): EndlessRecyclerOnScrollListener { - enabled = false - return this - } - - @JvmOverloads - fun resetPageCount(page: Int = 0) { - previousTotal = 0 - isLoading = true - currentPage = page - onLoadMore(currentPage) - } - - abstract fun onLoadMore(currentPage: Int) -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/controller/CategoryCarouselController.kt b/app/src/main/java/com/aurora/store/view/epoxy/controller/CategoryCarouselController.kt deleted file mode 100644 index 0975afd97..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/controller/CategoryCarouselController.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.controller - -import com.aurora.gplayapi.data.models.StreamCluster - -class CategoryCarouselController(callbacks: Callbacks) : GenericCarouselController(callbacks) { - - override fun applyFilter(streamBundle: StreamCluster): Boolean { - return streamBundle.clusterAppList.isNotEmpty() // Filter empty clusters - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/controller/GenericCarouselController.kt b/app/src/main/java/com/aurora/store/view/epoxy/controller/GenericCarouselController.kt deleted file mode 100644 index f9d7ccb16..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/controller/GenericCarouselController.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.controller - -import com.airbnb.epoxy.TypedEpoxyController -import com.aurora.gplayapi.data.models.App -import com.aurora.gplayapi.data.models.StreamBundle -import com.aurora.gplayapi.data.models.StreamCluster -import com.aurora.store.R -import com.aurora.store.view.epoxy.groups.CarouselModelGroup -import com.aurora.store.view.epoxy.groups.CarouselShimmerGroup -import com.aurora.store.view.epoxy.views.app.AppListViewModel_ -import com.aurora.store.view.epoxy.views.app.NoAppViewModel_ - -open class GenericCarouselController(private val callbacks: Callbacks) : - - TypedEpoxyController() { - - interface Callbacks { - fun onHeaderClicked(streamCluster: StreamCluster) - fun onClusterScrolled(streamCluster: StreamCluster) - fun onAppClick(app: App) - fun onAppLongClick(app: App) - } - - open fun applyFilter(streamBundle: StreamCluster): Boolean { - return streamBundle.clusterTitle.isNotBlank() && - // Filter noisy cluster - streamBundle.clusterAppList.isNotEmpty() && - // Filter empty clusters - streamBundle.clusterAppList.count() > 1 // Filter clusters with single apps (promotions) - } - - override fun buildModels(streamBundle: StreamBundle?) { - setFilterDuplicates(true) - if (streamBundle == null) { - for (i in 1..4) { - add( - CarouselShimmerGroup() - .id(i) - ) - } - } else { - if (streamBundle.streamClusters.isEmpty()) { - add( - NoAppViewModel_() - .id("no_app") - .icon(R.drawable.ic_apps) - .message(R.string.no_apps_available) - ) - } else { - if (streamBundle.streamClusters.size == 1) { - streamBundle - .streamClusters - .values - .filter { applyFilter(it) } - .forEach { streamCluster -> - streamCluster.clusterAppList.forEach { - add( - AppListViewModel_() - .id(it.id) - .app(it) - .click { _ -> callbacks.onAppClick(it) } - ) - } - } - } else { - streamBundle - .streamClusters - .values - .filter { applyFilter(it) } - .forEach { streamCluster -> - add(CarouselModelGroup(streamCluster, callbacks)) - } - } - if (streamBundle.hasNext()) { - add( - CarouselShimmerGroup() - .id("progress") - ) - } - } - } - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/groups/CarouselHorizontal.kt b/app/src/main/java/com/aurora/store/view/epoxy/groups/CarouselHorizontal.kt deleted file mode 100644 index 8a57ae393..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/groups/CarouselHorizontal.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.groups - -import android.content.Context -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.airbnb.epoxy.Carousel -import com.airbnb.epoxy.ModelView - -@ModelView(saveViewState = true, autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT) -class CarouselHorizontal(context: Context?) : Carousel(context) { - - override fun createLayoutManager(): LayoutManager = - LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) - - override fun getSnapHelperFactory(): Nothing? = null -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/groups/CarouselModelGroup.kt b/app/src/main/java/com/aurora/store/view/epoxy/groups/CarouselModelGroup.kt deleted file mode 100644 index ced922487..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/groups/CarouselModelGroup.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.groups - -import android.util.Log -import com.airbnb.epoxy.EpoxyModel -import com.airbnb.epoxy.EpoxyModelGroup -import com.aurora.gplayapi.data.models.StreamCluster -import com.aurora.store.R -import com.aurora.store.view.epoxy.controller.GenericCarouselController -import com.aurora.store.view.epoxy.views.HeaderViewModel_ -import com.aurora.store.view.epoxy.views.app.AppViewModel_ -import com.aurora.store.view.epoxy.views.shimmer.AppViewShimmerModel_ - -class CarouselModelGroup( - streamCluster: StreamCluster, - callbacks: GenericCarouselController.Callbacks -) : - EpoxyModelGroup( - R.layout.model_carousel_group, - buildModels( - streamCluster, - callbacks - ) - ) { - companion object { - private const val TAG = "CarouselModelGroup" - - private fun buildModels( - streamCluster: StreamCluster, - callbacks: GenericCarouselController.Callbacks - ): List> { - val models = ArrayList>() - val clusterViewModels = mutableListOf>() - - val idPrefix = streamCluster.clusterTitle.hashCode() - - models.add( - HeaderViewModel_() - .id("${idPrefix}_header") - .title(streamCluster.clusterTitle) - .browseUrl(streamCluster.clusterBrowseUrl) - .click { _ -> - callbacks.onHeaderClicked(streamCluster) - } - ) - - for (app in streamCluster.clusterAppList) { - clusterViewModels.add( - AppViewModel_() - .id(app.id) - .app(app) - .click { _ -> - callbacks.onAppClick(app) - } - .longClick { _ -> - callbacks.onAppLongClick(app) - false - } - .onBind { _, _, position -> - val itemCount = clusterViewModels.count() - if (itemCount >= 2) { - if (position == clusterViewModels.count() - 2) { - callbacks.onClusterScrolled(streamCluster) - Log.i(TAG, "Cluster ${streamCluster.clusterTitle} Scrolled") - } - } - } - ) - } - - if (streamCluster.hasNext()) { - clusterViewModels.add( - AppViewShimmerModel_() - .id("${idPrefix}_progress") - ) - } - - models.add( - CarouselHorizontalModel_() - .id("${idPrefix}_cluster") - .models(clusterViewModels) - ) - - return models - } - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/groups/CarouselShimmerGroup.kt b/app/src/main/java/com/aurora/store/view/epoxy/groups/CarouselShimmerGroup.kt deleted file mode 100644 index 8429a64f1..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/groups/CarouselShimmerGroup.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.groups - -import com.airbnb.epoxy.EpoxyModel -import com.airbnb.epoxy.EpoxyModelGroup -import com.aurora.store.R -import com.aurora.store.view.epoxy.views.shimmer.AppViewShimmerModel_ -import com.aurora.store.view.epoxy.views.shimmer.HeaderViewShimmerModel_ -import java.util.UUID - -class CarouselShimmerGroup : - EpoxyModelGroup( - R.layout.model_carousel_group, - buildModels() - ) { - companion object { - private fun buildModels(): List> { - val models = ArrayList>() - val clusterViewModels = mutableListOf>() - val idPrefix = UUID.randomUUID() - - for (i in 1..8) { - clusterViewModels.add( - AppViewShimmerModel_() - .id(i) - ) - } - - models.add( - HeaderViewShimmerModel_() - .id("shimmer_header") - ) - - models.add( - CarouselHorizontalModel_() - .id("cluster_$idPrefix") - .models(clusterViewModels) - ) - return models - } - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/AppProgressView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/AppProgressView.kt deleted file mode 100644 index 765baa871..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/AppProgressView.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.views - -import android.content.Context -import android.util.AttributeSet -import com.airbnb.epoxy.ModelView -import com.aurora.store.databinding.ViewAppProgressBinding - -@ModelView(autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT) -class AppProgressView @JvmOverloads constructor( - context: Context?, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : BaseView(context, attrs, defStyleAttr) diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/BaseModel.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/BaseModel.kt deleted file mode 100644 index 919aedd03..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/BaseModel.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.views - -import android.view.View -import android.view.animation.AnimationUtils -import com.airbnb.epoxy.EpoxyModel -import com.aurora.store.view.epoxy.views.app.AppListView - -abstract class BaseModel : EpoxyModel() { - - override fun bind(view: T) { - super.bind(view) - when (view) { - is AppListView -> { - view.startAnimation( - AnimationUtils.loadAnimation( - view.context, - android.R.anim.fade_in - ) - ) - } - } - } - - override fun unbind(view: T) { - when (view) { - is AppListView -> { - view.clearAnimation() - } - } - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/BaseView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/BaseView.kt deleted file mode 100644 index 8f6b86fd4..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/BaseView.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.aurora.store.view.epoxy.views - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.RelativeLayout -import androidx.viewbinding.ViewBinding -import java.lang.reflect.ParameterizedType - -abstract class BaseView : RelativeLayout { - - private lateinit var _binding: ViewBindingType - protected val binding get() = _binding - - constructor(context: Context?) : super(context) { - init(context) - } - - constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { - init(context) - } - - constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init(context) - } - - private fun init(context: Context?) { - if (context != null) { - _binding = inflateViewBinding(LayoutInflater.from(context)) - addView(_binding.root) - } - } - - @Suppress("UNCHECKED_CAST") - private fun inflateViewBinding(inflater: LayoutInflater): ViewBindingType { - val type = (javaClass.genericSuperclass as ParameterizedType) - .actualTypeArguments[0] as Class - val method = type.getMethod( - "inflate", - LayoutInflater::class.java, - ViewGroup::class.java, - Boolean::class.java - ) - return method.invoke(null, inflater, this, false) as ViewBindingType - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/CategoryView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/CategoryView.kt deleted file mode 100644 index 7f340c8fe..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/CategoryView.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.views - -import android.content.Context -import android.util.AttributeSet -import coil3.load -import com.airbnb.epoxy.CallbackProp -import com.airbnb.epoxy.ModelProp -import com.airbnb.epoxy.ModelView -import com.aurora.gplayapi.data.models.Category -import com.aurora.store.databinding.ViewCategoryBinding - -@ModelView( - autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT, - baseModelClass = BaseModel::class -) -class CategoryView @JvmOverloads constructor( - context: Context?, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : BaseView(context, attrs, defStyleAttr) { - - @ModelProp - fun category(category: Category) { - binding.txtName.text = category.title - binding.imgBackground.load(category.imageUrl) - } - - @CallbackProp - fun click(onClickListener: OnClickListener?) { - binding.root.setOnClickListener(onClickListener) - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/HeaderView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/HeaderView.kt deleted file mode 100644 index 1d8b097d1..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/HeaderView.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.views - -import android.content.Context -import android.util.AttributeSet -import com.airbnb.epoxy.CallbackProp -import com.airbnb.epoxy.ModelProp -import com.airbnb.epoxy.ModelView -import com.airbnb.epoxy.OnViewRecycled -import com.aurora.store.databinding.ViewHeaderBinding - -@ModelView( - autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT, - baseModelClass = BaseModel::class -) -class HeaderView @JvmOverloads constructor( - context: Context?, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : BaseView(context, attrs, defStyleAttr) { - - @ModelProp - fun title(title: String) { - binding.txtTitle.text = title - } - - @JvmOverloads - @ModelProp - fun browseUrl(browseUrl: String? = String()) { - if (browseUrl.isNullOrEmpty()) { - binding.imgAction.visibility = INVISIBLE - } - } - - @CallbackProp - fun click(onClickListener: OnClickListener?) { - binding.root.setOnClickListener(onClickListener) - } - - @OnViewRecycled - fun clear() { - binding.imgAction.visibility = VISIBLE - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/UpdateHeaderView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/UpdateHeaderView.kt deleted file mode 100644 index 2f9607652..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/UpdateHeaderView.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.views - -import android.content.Context -import android.util.AttributeSet -import com.airbnb.epoxy.CallbackProp -import com.airbnb.epoxy.ModelProp -import com.airbnb.epoxy.ModelView -import com.airbnb.epoxy.OnViewRecycled -import com.aurora.store.databinding.ViewHeaderUpdateBinding - -@ModelView( - autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT, - baseModelClass = BaseModel::class -) -class UpdateHeaderView @JvmOverloads constructor( - context: Context?, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : BaseView(context, attrs, defStyleAttr) { - - @ModelProp - fun title(title: String) { - binding.txtTitle.text = title - } - - @ModelProp - fun action(action: String) { - binding.btnAction.text = action - } - - @CallbackProp - fun click(onClickListener: OnClickListener?) { - binding.btnAction.setOnClickListener(onClickListener) - } - - @OnViewRecycled - fun clear() { - binding.btnAction.isEnabled = true - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppListView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppListView.kt deleted file mode 100644 index 105026465..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppListView.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.views.app - -import android.content.Context -import android.util.AttributeSet -import androidx.core.content.ContextCompat -import coil3.load -import coil3.request.placeholder -import coil3.request.transformations -import coil3.transform.RoundedCornersTransformation -import com.airbnb.epoxy.CallbackProp -import com.airbnb.epoxy.ModelProp -import com.airbnb.epoxy.ModelView -import com.aurora.gplayapi.data.models.App -import com.aurora.store.R -import com.aurora.store.databinding.ViewAppListBinding -import com.aurora.store.util.CommonUtil -import com.aurora.store.view.epoxy.views.BaseModel -import com.aurora.store.view.epoxy.views.BaseView - -@ModelView( - autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT, - baseModelClass = BaseModel::class -) -class AppListView @JvmOverloads constructor( - context: Context?, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : BaseView(context, attrs, defStyleAttr) { - - @ModelProp - fun app(app: App) { - binding.imgIcon.load(app.iconArtwork.url) { - placeholder(R.drawable.bg_placeholder) - transformations(RoundedCornersTransformation(25F)) - } - - binding.txtLine1.text = app.displayName - binding.txtLine2.text = app.developerName - - val extras: MutableList = mutableListOf() - extras.add(if (app.size > 0) CommonUtil.addSiPrefix(app.size) else app.downloadString) - extras.add("${app.labeledRating}★") - extras.add( - if (app.isFree) { - ContextCompat.getString(context, R.string.details_free) - } else { - ContextCompat.getString(context, R.string.details_paid) - } - ) - - if (app.containsAds) { - extras.add(ContextCompat.getString(context, R.string.details_contains_ads)) - } - - if (app.dependencies.dependentPackages.isNotEmpty()) { - extras.add(ContextCompat.getString(context, R.string.details_gsf_dependent)) - } - - binding.txtLine3.text = extras.joinToString(separator = " • ") - } - - @CallbackProp - fun click(onClickListener: OnClickListener?) { - binding.root.setOnClickListener(onClickListener) - } - - @CallbackProp - fun longClick(onClickListener: OnLongClickListener?) { - binding.root.setOnLongClickListener(onClickListener) - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppUpdateView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppUpdateView.kt deleted file mode 100644 index 693f67ca4..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppUpdateView.kt +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.views.app - -import android.animation.ObjectAnimator -import android.content.Context -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.View -import android.view.animation.AccelerateDecelerateInterpolator -import androidx.core.content.ContextCompat -import androidx.core.text.HtmlCompat -import androidx.core.view.isVisible -import coil3.asDrawable -import coil3.load -import coil3.request.placeholder -import coil3.request.transformations -import coil3.transform.CircleCropTransformation -import coil3.transform.RoundedCornersTransformation -import com.airbnb.epoxy.CallbackProp -import com.airbnb.epoxy.ModelProp -import com.airbnb.epoxy.ModelView -import com.airbnb.epoxy.OnViewRecycled -import com.aurora.extensions.invisible -import com.aurora.extensions.px -import com.aurora.store.R -import com.aurora.store.data.model.DownloadStatus -import com.aurora.store.data.room.download.Download -import com.aurora.store.data.room.update.Update -import com.aurora.store.databinding.ViewAppUpdateBinding -import com.aurora.store.util.CommonUtil -import com.aurora.store.view.epoxy.views.BaseModel -import com.aurora.store.view.epoxy.views.BaseView - -@ModelView( - autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT, - baseModelClass = BaseModel::class -) -class AppUpdateView @JvmOverloads constructor( - context: Context?, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : BaseView(context, attrs, defStyleAttr) { - private var iconDrawable: Drawable? = null - private val cornersTransformation = RoundedCornersTransformation(8.px.toFloat()) - - @ModelProp - fun update(update: Update) { - /*Inflate App details*/ - with(update) { - binding.txtLine1.text = displayName - binding.imgIcon.load(iconURL) { - placeholder(R.drawable.bg_placeholder) - transformations(cornersTransformation) - listener { _, result -> - result.image.asDrawable(resources).let { iconDrawable = it } - } - } - - binding.txtLine2.text = developerName - binding.txtLine3.text = ("${CommonUtil.addSiPrefix(size)} • $updatedOn") - binding.txtLine4.text = ("$versionName ($versionCode)") - binding.txtChangelog.text = if (changelog.isNotEmpty()) { - HtmlCompat.fromHtml( - changelog, - HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS - ) - } else { - context.getString(R.string.details_changelog_unavailable) - } - - binding.headerIndicator.setOnClickListener { - if (binding.cardChangelog.isVisible) { - binding.headerIndicator.icon = - ContextCompat.getDrawable(context, R.drawable.ic_arrow_down) - binding.cardChangelog.visibility = View.GONE - } else { - binding.headerIndicator.icon = - ContextCompat.getDrawable(context, R.drawable.ic_arrow_up) - binding.cardChangelog.visibility = View.VISIBLE - } - } - } - } - - @ModelProp - fun download(download: Download?) { - if (download != null) { - binding.btnAction.updateState(download.status) - when (download.status) { - DownloadStatus.VERIFYING, - DownloadStatus.QUEUED -> { - binding.progressDownload.isIndeterminate = true - animateImageView(scaleFactor = 0.75f) - } - - DownloadStatus.DOWNLOADING -> { - binding.progressDownload.isIndeterminate = false - binding.progressDownload.progress = download.progress - animateImageView(scaleFactor = 0.75f) - } - - else -> { - binding.progressDownload.isIndeterminate = true - animateImageView(scaleFactor = 1f) - } - } - } - } - - @CallbackProp - fun click(onClickListener: OnClickListener?) { - binding.layoutContent.setOnClickListener(onClickListener) - } - - @CallbackProp - fun positiveAction(onClickListener: OnClickListener?) { - binding.btnAction.addPositiveOnClickListener(onClickListener) - } - - @CallbackProp - fun negativeAction(onClickListener: OnClickListener?) { - binding.btnAction.addNegativeOnClickListener(onClickListener) - } - - @CallbackProp - fun longClick(onClickListener: OnLongClickListener?) { - binding.layoutContent.setOnLongClickListener(onClickListener) - } - - @OnViewRecycled - fun clear() { - iconDrawable = null - - binding.apply { - headerIndicator.removeCallbacks {} - progressDownload.invisible() - btnAction.apply { - removeCallbacks { } - updateState(DownloadStatus.UNAVAILABLE) - } - } - } - - private fun animateImageView(scaleFactor: Float = 1f) { - val isDownloadVisible = binding.progressDownload.isShown - - // Avoids flickering when the download is in progress - if (isDownloadVisible && scaleFactor != 1f) { - return - } - - if (!isDownloadVisible && scaleFactor == 1f) { - return - } - - if (scaleFactor == 1f) { - binding.progressDownload.invisible() - } else { - binding.progressDownload.show() - } - - val scale = listOf( - ObjectAnimator.ofFloat(binding.imgIcon, "scaleX", scaleFactor), - ObjectAnimator.ofFloat(binding.imgIcon, "scaleY", scaleFactor) - ) - - scale.forEach { animation -> - animation.apply { - interpolator = AccelerateDecelerateInterpolator() - duration = 250 - start() - } - } - - iconDrawable?.let { - binding.imgIcon.load(it) { - transformations( - if (scaleFactor == 1f) { - cornersTransformation - } else { - CircleCropTransformation() - } - ) - } - } - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppView.kt deleted file mode 100644 index 5ea989447..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/app/AppView.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.views.app - -import android.content.Context -import android.util.AttributeSet -import coil3.load -import coil3.request.placeholder -import coil3.request.transformations -import coil3.transform.RoundedCornersTransformation -import com.airbnb.epoxy.CallbackProp -import com.airbnb.epoxy.ModelProp -import com.airbnb.epoxy.ModelView -import com.aurora.gplayapi.data.models.App -import com.aurora.store.R -import com.aurora.store.databinding.ViewAppBinding -import com.aurora.store.util.CommonUtil -import com.aurora.store.view.epoxy.views.BaseModel -import com.aurora.store.view.epoxy.views.BaseView - -@ModelView( - autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT, - baseModelClass = BaseModel::class -) -class AppView @JvmOverloads constructor( - context: Context?, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : BaseView(context, attrs, defStyleAttr) { - - @ModelProp - fun app(app: App) { - binding.txtName.text = app.displayName - binding.imgIcon.load(app.iconArtwork.url) { - placeholder(R.drawable.bg_placeholder) - transformations(RoundedCornersTransformation(32F)) - } - - if (app.size > 0) { - binding.txtSize.text = CommonUtil.addSiPrefix(app.size) - } else { - binding.txtSize.text = app.downloadString - } - } - - @CallbackProp - fun click(onClickListener: OnClickListener?) { - binding.root.setOnClickListener(onClickListener) - } - - @CallbackProp - fun longClick(onClickListener: OnLongClickListener?) { - binding.root.setOnLongClickListener(onClickListener) - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/app/NoAppView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/app/NoAppView.kt deleted file mode 100644 index dbb9efa88..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/app/NoAppView.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.views.app - -import android.content.Context -import android.util.AttributeSet -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.core.view.isVisible -import coil3.load -import com.airbnb.epoxy.CallbackProp -import com.airbnb.epoxy.ModelProp -import com.airbnb.epoxy.ModelView -import com.aurora.store.databinding.ViewNoAppBinding -import com.aurora.store.view.epoxy.views.BaseModel -import com.aurora.store.view.epoxy.views.BaseView - -@ModelView( - autoLayout = ModelView.Size.MATCH_WIDTH_MATCH_HEIGHT, - baseModelClass = BaseModel::class -) -class NoAppView @JvmOverloads constructor( - context: Context?, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : BaseView(context, attrs, defStyleAttr) { - - @ModelProp - fun message(@StringRes message: Int) { - binding.txt.text = context.getString(message) - } - - @ModelProp - fun icon(@DrawableRes icon: Int) { - binding.img.load(icon) - } - - @JvmOverloads - @ModelProp - fun showAction(visibility: Boolean = false) { - binding.button.isVisible = visibility - } - - @JvmOverloads - @ModelProp - fun actionMessage(@StringRes message: Int? = null) { - message?.let { binding.button.text = context.getString(message) } - } - - @JvmOverloads - @CallbackProp - fun actionCallback(viewOnClickListener: OnClickListener? = null) { - binding.button.setOnClickListener(viewOnClickListener) - } -} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/shimmer/AppListViewShimmer.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/shimmer/AppListViewShimmer.kt deleted file mode 100644 index 58baf31cf..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/shimmer/AppListViewShimmer.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.views.shimmer - -import android.content.Context -import android.util.AttributeSet -import com.airbnb.epoxy.ModelView -import com.aurora.store.databinding.ViewAppListShimmerBinding -import com.aurora.store.view.epoxy.views.BaseModel -import com.aurora.store.view.epoxy.views.BaseView - -@ModelView( - autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT, - baseModelClass = BaseModel::class -) -class AppListViewShimmer @JvmOverloads constructor( - context: Context?, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : BaseView(context, attrs, defStyleAttr) diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/shimmer/AppViewShimmer.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/shimmer/AppViewShimmer.kt deleted file mode 100644 index 79ad12368..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/shimmer/AppViewShimmer.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.views.shimmer - -import android.content.Context -import android.util.AttributeSet -import com.airbnb.epoxy.ModelView -import com.aurora.store.databinding.ViewAppShimmerBinding -import com.aurora.store.view.epoxy.views.BaseModel -import com.aurora.store.view.epoxy.views.BaseView - -@ModelView( - autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT, - baseModelClass = BaseModel::class -) -class AppViewShimmer @JvmOverloads constructor( - context: Context?, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : BaseView(context, attrs, defStyleAttr) diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/shimmer/HeaderViewShimmer.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/shimmer/HeaderViewShimmer.kt deleted file mode 100644 index ed8479d78..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/shimmer/HeaderViewShimmer.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.epoxy.views.shimmer - -import android.content.Context -import android.util.AttributeSet -import com.airbnb.epoxy.ModelView -import com.aurora.store.databinding.ViewHeaderShimmerBinding -import com.aurora.store.view.epoxy.views.BaseModel -import com.aurora.store.view.epoxy.views.BaseView - -@ModelView( - autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT, - baseModelClass = BaseModel::class -) -class HeaderViewShimmer @JvmOverloads constructor( - context: Context?, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : BaseView(context, attrs, defStyleAttr) From 6dc1cf2a8c5b5568d1d3e85cc1324abb64adaf7b Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Tue, 19 May 2026 12:14:03 +0530 Subject: [PATCH 12/27] legacy: remove fragment-based screens and sheets Drops every Fragment, DialogFragment and BottomSheet in view/ui plus the flavoured SplashFragment variants. All call sites now route through compose/ui equivalents and ComposeActivity. --- .../store/view/ui/splash/SplashFragment.kt | 38 -- .../store/view/ui/account/GoogleFragment.kt | 134 ----- .../view/ui/apps/AppsContainerFragment.kt | 148 ------ .../store/view/ui/commons/BaseFragment.kt | 153 ------ .../view/ui/commons/CategoryBrowseFragment.kt | 100 ---- .../store/view/ui/commons/CategoryFragment.kt | 104 ---- .../commons/ExpandedStreamBrowseFragment.kt | 109 ----- .../store/view/ui/commons/ForYouFragment.kt | 113 ----- .../view/ui/commons/ForceRestartDialog.kt | 27 - .../view/ui/commons/MoreDialogFragment.kt | 460 ------------------ .../ui/commons/TopChartContainerFragment.kt | 102 ---- .../store/view/ui/commons/TopChartFragment.kt | 138 ------ .../view/ui/details/DevProfileFragment.kt | 107 ---- .../view/ui/games/GamesContainerFragment.kt | 147 ------ .../ui/preferences/BasePreferenceFragment.kt | 49 -- .../view/ui/preferences/SettingsFragment.kt | 71 --- .../store/view/ui/preferences/UIPreference.kt | 66 --- .../installation/InstallationPreference.kt | 81 --- .../preferences/network/NetworkPreference.kt | 81 --- .../ui/preferences/network/ProxyURLDialog.kt | 97 ---- .../preferences/updates/UpdatesPreference.kt | 161 ------ .../updates/UpdatesRestrictionsDialog.kt | 72 --- .../store/view/ui/sheets/AppMenuSheet.kt | 118 ----- .../store/view/ui/sheets/BaseDialogSheet.kt | 70 --- .../store/view/ui/sheets/DeviceMiuiSheet.kt | 56 --- .../view/ui/sheets/NetworkDialogSheet.kt | 60 --- .../ui/splash/BaseFlavouredSplashFragment.kt | 268 ---------- .../store/view/ui/updates/UpdatesFragment.kt | 234 --------- .../store/view/ui/splash/SplashFragment.kt | 25 - .../store/view/ui/splash/SplashFragment.kt | 25 - 30 files changed, 3414 deletions(-) delete mode 100644 app/src/huawei/java/com/aurora/store/view/ui/splash/SplashFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/account/GoogleFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/apps/AppsContainerFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/commons/BaseFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/commons/CategoryBrowseFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/commons/CategoryFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/commons/ExpandedStreamBrowseFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/commons/ForYouFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/commons/ForceRestartDialog.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/commons/TopChartContainerFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/commons/TopChartFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/details/DevProfileFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/games/GamesContainerFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/preferences/BasePreferenceFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/preferences/SettingsFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/preferences/UIPreference.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/preferences/installation/InstallationPreference.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/preferences/network/NetworkPreference.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/preferences/network/ProxyURLDialog.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/preferences/updates/UpdatesPreference.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/preferences/updates/UpdatesRestrictionsDialog.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/sheets/AppMenuSheet.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/sheets/BaseDialogSheet.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/sheets/DeviceMiuiSheet.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/sheets/NetworkDialogSheet.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/splash/BaseFlavouredSplashFragment.kt delete mode 100644 app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt delete mode 100644 app/src/preload/java/com/aurora/store/view/ui/splash/SplashFragment.kt delete mode 100644 app/src/vanilla/java/com/aurora/store/view/ui/splash/SplashFragment.kt diff --git a/app/src/huawei/java/com/aurora/store/view/ui/splash/SplashFragment.kt b/app/src/huawei/java/com/aurora/store/view/ui/splash/SplashFragment.kt deleted file mode 100644 index 2b5f789be..000000000 --- a/app/src/huawei/java/com/aurora/store/view/ui/splash/SplashFragment.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.ui.splash - -import com.aurora.extensions.hide -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class SplashFragment : BaseFlavouredSplashFragment() { - override fun attachActions() { - super.attachActions() - - binding.btnAnonymous.hide() - } - - override fun resetActions() { - super.resetActions() - - binding.btnAnonymous.hide() - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/account/GoogleFragment.kt b/app/src/main/java/com/aurora/store/view/ui/account/GoogleFragment.kt deleted file mode 100644 index 3ba18ccdb..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/account/GoogleFragment.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.ui.account - -import android.os.Build -import android.os.Bundle -import android.view.View -import android.webkit.CookieManager -import android.webkit.WebChromeClient -import android.webkit.WebSettings -import android.webkit.WebView -import android.webkit.WebViewClient -import android.widget.Toast -import androidx.core.view.isVisible -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import com.aurora.gplayapi.helpers.AuthHelper -import com.aurora.store.AuroraApp -import com.aurora.store.R -import com.aurora.store.data.event.AuthEvent -import com.aurora.store.databinding.FragmentGoogleBinding -import com.aurora.store.util.AC2DMUtil -import com.aurora.store.view.ui.commons.BaseFragment -import com.aurora.store.viewmodel.auth.AuthViewModel -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch - -@AndroidEntryPoint -class GoogleFragment : BaseFragment() { - - private val viewModel: AuthViewModel by activityViewModels() - - companion object { - const val EMBEDDED_SETUP_URL = "https://accounts.google.com/EmbeddedSetup" - const val AUTH_TOKEN = "oauth_token" - private const val JS_SCRIPT = - "(function() { return document.getElementById('profileIdentifier').innerHTML; })();" - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val cookieManager = CookieManager.getInstance() - - binding.webview.apply { - cookieManager.removeAllCookies(null) - cookieManager.acceptThirdPartyCookies(this) - cookieManager.setAcceptThirdPartyCookies(this, true) - - webChromeClient = object : WebChromeClient() { - override fun onProgressChanged(view: WebView?, newProgress: Int) { - super.onProgressChanged(view, newProgress) - - if (newProgress != 0) { - binding.progressBar.also { - it.isVisible = newProgress < 100 - it.isIndeterminate = false - it.max = 100 - it.progress = newProgress - } - } else { - binding.progressBar.isIndeterminate = true - } - } - } - - webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView, url: String) { - val cookies = CookieManager.getInstance().getCookie(url) - // cookies can be null if there is an error - if (cookies != null) { - val cookieMap = AC2DMUtil.parseCookieString(cookies) - if (cookieMap.isNotEmpty() && cookieMap[AUTH_TOKEN] != null) { - val oauthToken = cookieMap[AUTH_TOKEN] - evaluateJavascript(JS_SCRIPT) { - val email = it.replace("\"".toRegex(), "") - viewModel.buildAuthData(view.context, email, oauthToken) - } - } - } - } - } - - settings.apply { - allowContentAccess = true - domStorageEnabled = true - javaScriptEnabled = true - cacheMode = WebSettings.LOAD_DEFAULT - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) safeBrowsingEnabled = false - } - loadUrl(EMBEDDED_SETUP_URL) - } - - viewLifecycleOwner.lifecycleScope.launch { - AuroraApp.events.authEvent.collect { event -> - if (event is AuthEvent.GoogleLogin) onEventReceived(event) - } - } - } - - private fun onEventReceived(event: AuthEvent.GoogleLogin) { - if (event.success) { - viewModel.buildGoogleAuthData(event.email, event.token, AuthHelper.Token.AAS) - } else { - Toast.makeText( - requireContext(), - getString(R.string.toast_aas_token_failed), - Toast.LENGTH_LONG - ).show() - } - - findNavController().navigate( - GoogleFragmentDirections.actionGoogleFragmentToSplashFragment() - ) - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/apps/AppsContainerFragment.kt b/app/src/main/java/com/aurora/store/view/ui/apps/AppsContainerFragment.kt deleted file mode 100644 index d88f07fca..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/apps/AppsContainerFragment.kt +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.ui.apps - -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updateLayoutParams -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.navigation.fragment.findNavController -import androidx.viewpager2.adapter.FragmentStateAdapter -import com.aurora.extensions.navigate -import com.aurora.store.MobileNavigationDirections -import com.aurora.store.R -import com.aurora.store.compose.navigation.Screen -import com.aurora.store.databinding.FragmentAppsGamesBinding -import com.aurora.store.util.Preferences -import com.aurora.store.view.ui.commons.BaseFragment -import com.aurora.store.view.ui.commons.CategoryFragment -import com.aurora.store.view.ui.commons.ForYouFragment -import com.aurora.store.view.ui.commons.TopChartContainerFragment -import com.aurora.store.viewmodel.apps.AppsContainerViewModel -import com.google.android.material.tabs.TabLayout -import com.google.android.material.tabs.TabLayoutMediator -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class AppsContainerFragment : BaseFragment() { - - private val viewModel: AppsContainerViewModel by viewModels() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - // Adjust FAB margins for edgeToEdge display - ViewCompat.setOnApplyWindowInsetsListener(binding.searchFab) { _, windowInsets -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()) - binding.searchFab.updateLayoutParams { - bottomMargin = insets.bottom + resources.getDimensionPixelSize(R.dimen.margin_large) - } - WindowInsetsCompat.CONSUMED - } - - // Toolbar - binding.toolbar.apply { - title = getString(R.string.title_apps) - setOnMenuItemClickListener { - when (it.itemId) { - R.id.menu_download_manager -> { - requireContext().navigate(Screen.Downloads) - } - - R.id.menu_more -> { - findNavController().navigate( - MobileNavigationDirections.actionGlobalMoreDialogFragment() - ) - } - } - true - } - } - - // ViewPager - val isForYouEnabled = Preferences.getBoolean( - requireContext(), - Preferences.PREFERENCE_FOR_YOU - ) - - binding.pager.adapter = ViewPagerAdapter( - childFragmentManager, - viewLifecycleOwner.lifecycle, - !viewModel.authProvider.isAnonymous, - isForYouEnabled - ) - - binding.pager.isUserInputEnabled = - false // Disable viewpager scroll to avoid scroll conflicts - - val tabTitles: MutableList = mutableListOf().apply { - if (isForYouEnabled) { - add(getString(R.string.tab_for_you)) - } - - add(getString(R.string.tab_top_charts)) - add(getString(R.string.tab_categories)) - } - - TabLayoutMediator( - binding.tabLayout, - binding.pager, - true - ) { tab: TabLayout.Tab, position: Int -> - tab.text = tabTitles[position] - }.attach() - - binding.searchFab.setOnClickListener { - requireContext().navigate(Screen.Search) - } - } - - override fun onDestroyView() { - binding.pager.adapter = null - super.onDestroyView() - } - - internal class ViewPagerAdapter( - fragment: FragmentManager, - lifecycle: Lifecycle, - private val isGoogleAccount: Boolean, - private val isForYouEnabled: Boolean - ) : - FragmentStateAdapter(fragment, lifecycle) { - - private val tabFragments: MutableList = mutableListOf().apply { - if (isForYouEnabled) { - add(ForYouFragment.newInstance(0)) - } - add(TopChartContainerFragment.newInstance(0)) - add(CategoryFragment.newInstance(0)) - } - - override fun createFragment(position: Int): Fragment = tabFragments[position] - - override fun getItemCount(): Int = tabFragments.size - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/BaseFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/BaseFragment.kt deleted file mode 100644 index ccd05d12f..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/commons/BaseFragment.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.ui.commons - -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController -import androidx.viewbinding.ViewBinding -import com.airbnb.epoxy.EpoxyRecyclerView -import com.aurora.extensions.TAG -import com.aurora.extensions.navigate -import com.aurora.gplayapi.data.models.Category -import com.aurora.gplayapi.data.models.StreamCluster -import com.aurora.store.MobileNavigationDirections -import com.aurora.store.compose.navigation.Screen -import com.aurora.store.data.model.MinimalApp -import com.aurora.store.data.providers.PermissionProvider -import java.lang.reflect.ParameterizedType - -abstract class BaseFragment : Fragment() { - - lateinit var permissionProvider: PermissionProvider - - protected open var viewBindingType: ViewBindingType? = null - protected val binding get() = viewBindingType!! - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - permissionProvider = PermissionProvider(this) - } - - @Suppress("UNCHECKED_CAST") - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val type = (javaClass.genericSuperclass as ParameterizedType) - .actualTypeArguments[0] as Class - val method = type.getMethod( - "inflate", - LayoutInflater::class.java, - ViewGroup::class.java, - Boolean::class.java - ) - viewBindingType = method.invoke(null, inflater, container, false) as ViewBindingType - - return binding.root - } - - override fun onDestroy() { - permissionProvider.unregister() - super.onDestroy() - } - - override fun onDestroyView() { - cleanupRecyclerViews(findAllRecyclerViews(requireView())) - viewBindingType = null - super.onDestroyView() - } - - fun openDetailsFragment(packageName: String) { - requireContext().navigate( - Screen.AppDetails(packageName) - ) - } - - fun openCategoryBrowseFragment(category: Category) { - findNavController().navigate( - MobileNavigationDirections.actionGlobalCategoryBrowseFragment( - category.title, - category.browseUrl - ) - ) - } - - fun openStreamBrowseFragment(browseUrl: String, title: String = "") { - if (browseUrl.lowercase().contains("expanded")) { - findNavController().navigate( - MobileNavigationDirections.actionGlobalExpandedStreamBrowseFragment( - title, - browseUrl - ) - ) - } else if (browseUrl.lowercase().contains("developer")) { - findNavController().navigate( - MobileNavigationDirections.actionGlobalDevProfileFragment( - browseUrl.substringAfter("developer-"), - title - ) - ) - } - } - - fun openStreamBrowseFragment(streamCluster: StreamCluster) { - requireContext().navigate(Screen.StreamBrowse(streamCluster)) - } - - fun openAppMenuSheet(app: MinimalApp) { - findNavController().navigate(MobileNavigationDirections.actionGlobalAppMenuSheet(app)) - } - - fun openGMSWarningFragment() { - // TODO: FIX ME - } - - private fun cleanupRecyclerViews(recyclerViews: List) { - recyclerViews.forEach { recyclerView -> - runCatching { - recyclerView.adapter?.let { - recyclerView.swapAdapter(it, true) - } - }.onFailure { - Log.e(TAG, "Failed to cleanup RecyclerView", it) - } - } - } - - private fun findAllRecyclerViews(view: View): List { - val recyclerViews = mutableListOf() - - if (view is EpoxyRecyclerView) { - recyclerViews.add(view) - } else if (view is ViewGroup) { - for (i in 0 until view.childCount) { - recyclerViews.addAll(findAllRecyclerViews(view.getChildAt(i))) - } - } - - return recyclerViews - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/CategoryBrowseFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/CategoryBrowseFragment.kt deleted file mode 100644 index 319631470..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/commons/CategoryBrowseFragment.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.ui.commons - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.aurora.gplayapi.data.models.App -import com.aurora.gplayapi.data.models.StreamBundle -import com.aurora.gplayapi.data.models.StreamCluster -import com.aurora.store.data.model.ViewState -import com.aurora.store.data.model.ViewState.Loading.getDataAs -import com.aurora.store.databinding.FragmentGenericWithToolbarBinding -import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener -import com.aurora.store.view.epoxy.controller.CategoryCarouselController -import com.aurora.store.view.epoxy.controller.GenericCarouselController -import com.aurora.store.viewmodel.subcategory.CategoryStreamViewModel -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class CategoryBrowseFragment : - BaseFragment(), - GenericCarouselController.Callbacks { - private val args: CategoryBrowseFragmentArgs by navArgs() - private val viewModel: CategoryStreamViewModel by activityViewModels() - - private var streamBundle: StreamBundle? = StreamBundle() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val genericCarouselController = CategoryCarouselController(this) - - // Toolbar - binding.toolbar.apply { - title = args.title - setNavigationOnClickListener { findNavController().navigateUp() } - } - - // RecyclerView - binding.recycler.setController(genericCarouselController) - binding.recycler.addOnScrollListener(object : EndlessRecyclerOnScrollListener() { - override fun onLoadMore(currentPage: Int) { - viewModel.observe(args.browseUrl) - } - }) - - viewModel.getStreamBundle(args.browseUrl) - viewModel.liveData.observe(viewLifecycleOwner) { - when (it) { - is ViewState.Loading -> { - genericCarouselController.setData(null) - } - - is ViewState.Success<*> -> { - val stash = it.getDataAs>() - streamBundle = stash[args.browseUrl] - - genericCarouselController.setData(streamBundle) - } - - else -> {} - } - } - } - - override fun onHeaderClicked(streamCluster: StreamCluster) { - openStreamBrowseFragment(streamCluster) - } - - override fun onClusterScrolled(streamCluster: StreamCluster) { - viewModel.observeCluster(args.browseUrl, streamCluster) - } - - override fun onAppClick(app: App) { - openDetailsFragment(app.packageName) - } - - override fun onAppLongClick(app: App) { - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/CategoryFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/CategoryFragment.kt deleted file mode 100644 index a81c6c344..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/commons/CategoryFragment.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.ui.commons - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.viewModels -import com.aurora.Constants -import com.aurora.gplayapi.data.models.Category -import com.aurora.store.CategoryStash -import com.aurora.store.data.model.ViewState -import com.aurora.store.data.model.ViewState.Empty.getDataAs -import com.aurora.store.databinding.FragmentGenericRecyclerBinding -import com.aurora.store.view.epoxy.views.CategoryViewModel_ -import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ -import com.aurora.store.viewmodel.category.CategoryViewModel -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class CategoryFragment : BaseFragment() { - private val viewModel: CategoryViewModel by viewModels() - - companion object { - @JvmStatic - fun newInstance(pageType: Int): CategoryFragment = CategoryFragment().apply { - arguments = Bundle().apply { - putInt(Constants.PAGE_TYPE, pageType) - } - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - var pageType = 0 - val bundle = arguments - if (bundle != null) { - pageType = bundle.getInt(Constants.PAGE_TYPE, 0) - } - - when (pageType) { - 0 -> viewModel.getCategoryList(Category.Type.APPLICATION) - 1 -> viewModel.getCategoryList(Category.Type.GAME) - } - - val categoryType = when (pageType) { - 1 -> Category.Type.GAME - else -> Category.Type.APPLICATION - } - - viewModel.liveData.observe(viewLifecycleOwner) { - when (it) { - is ViewState.Success<*> -> { - val stash = it.getDataAs() - updateController(stash[categoryType]) - } - - else -> { - updateController(emptyList()) - } - } - } - } - - fun updateController(categories: List?) { - binding.recycler.withModels { - if (categories == null) { - for (i in 1..10) { - add( - AppListViewShimmerModel_() - .id(i) - ) - } - } else { - setFilterDuplicates(true) - categories.forEach { - add( - CategoryViewModel_() - .id(it.title) - .category(it) - .click { _ -> openCategoryBrowseFragment(it) } - ) - } - } - } - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/ExpandedStreamBrowseFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/ExpandedStreamBrowseFragment.kt deleted file mode 100644 index 667cb6da0..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/commons/ExpandedStreamBrowseFragment.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.ui.commons - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.aurora.gplayapi.data.models.StreamCluster -import com.aurora.store.databinding.FragmentGenericWithToolbarBinding -import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener -import com.aurora.store.view.epoxy.views.AppProgressViewModel_ -import com.aurora.store.view.epoxy.views.app.AppListViewModel_ -import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ -import com.aurora.store.viewmodel.browse.ExpandedStreamBrowseViewModel -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class ExpandedStreamBrowseFragment : BaseFragment() { - private val args: ExpandedStreamBrowseFragmentArgs by navArgs() - private val viewModel: ExpandedStreamBrowseViewModel by viewModels() - - private lateinit var endlessRecyclerOnScrollListener: EndlessRecyclerOnScrollListener - private lateinit var cluster: StreamCluster - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - // Toolbar - binding.toolbar.apply { - title = args.title - setNavigationOnClickListener { findNavController().navigateUp() } - } - - viewModel.liveData.observe(viewLifecycleOwner) { - if (!::cluster.isInitialized) attachRecycler() - cluster = it - - updateController(cluster) - updateTitle(cluster) - } - - viewModel.getInitialCluster(args.expandedStreamUrl) - updateController(null) - } - - private fun updateTitle(streamCluster: StreamCluster) { - if (streamCluster.clusterTitle.isNotEmpty()) { - binding.toolbar.title = streamCluster.clusterTitle - } - } - - private fun attachRecycler() { - endlessRecyclerOnScrollListener = object : EndlessRecyclerOnScrollListener() { - override fun onLoadMore(currentPage: Int) { - viewModel.next() - } - } - binding.recycler.addOnScrollListener(endlessRecyclerOnScrollListener) - } - - private fun updateController(streamCluster: StreamCluster?) { - binding.recycler.withModels { - setFilterDuplicates(true) - if (streamCluster == null) { - for (i in 1..6) { - add( - AppListViewShimmerModel_() - .id(i) - ) - } - } else { - streamCluster.clusterAppList.forEach { - add( - AppListViewModel_() - .id(it.packageName.hashCode()) - .app(it) - .click { _ -> openDetailsFragment(it.packageName) } - ) - } - - if (streamCluster.hasNext()) { - add( - AppProgressViewModel_() - .id("progress") - ) - } - } - } - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/ForYouFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/ForYouFragment.kt deleted file mode 100644 index e8171bb74..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/commons/ForYouFragment.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * Aurora Store is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Aurora Store. If not, see . - * - */ - -package com.aurora.store.view.ui.commons - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.activityViewModels -import com.aurora.Constants -import com.aurora.gplayapi.data.models.App -import com.aurora.gplayapi.data.models.StreamBundle -import com.aurora.gplayapi.data.models.StreamCluster -import com.aurora.gplayapi.helpers.contracts.StreamContract.Category -import com.aurora.gplayapi.helpers.contracts.StreamContract.Type -import com.aurora.store.HomeStash -import com.aurora.store.data.model.ViewState -import com.aurora.store.data.model.ViewState.Loading.getDataAs -import com.aurora.store.databinding.FragmentForYouBinding -import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener -import com.aurora.store.view.epoxy.controller.GenericCarouselController -import com.aurora.store.viewmodel.homestream.StreamViewModel -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class ForYouFragment : - BaseFragment(), - GenericCarouselController.Callbacks { - private val viewModel: StreamViewModel by activityViewModels() - - private var category: Category = Category.APPLICATION - private var streamBundle: StreamBundle? = StreamBundle() - - companion object { - @JvmStatic - fun newInstance(pageType: Int): ForYouFragment = ForYouFragment().apply { - arguments = Bundle().apply { - putInt(Constants.PAGE_TYPE, pageType) - } - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val genericCarouselController = GenericCarouselController(this) - - var pageType = 0 - val bundle = arguments - if (bundle != null) { - pageType = bundle.getInt(Constants.PAGE_TYPE, 0) - } - - category = if (pageType == 0) Category.APPLICATION else Category.GAME - - binding.recycler.setController(genericCarouselController) - binding.recycler.addOnScrollListener( - object : EndlessRecyclerOnScrollListener(visibleThreshold = 4) { - override fun onLoadMore(currentPage: Int) { - viewModel.observe(category, Type.HOME) - } - } - ) - - viewModel.getStreamBundle(category, Type.HOME) - viewModel.liveData.observe(viewLifecycleOwner) { - when (it) { - is ViewState.Loading -> { - genericCarouselController.setData(null) - } - - is ViewState.Success<*> -> { - val stash = it.getDataAs() - streamBundle = stash[category] - - genericCarouselController.setData(streamBundle) - } - - else -> {} - } - } - } - - override fun onHeaderClicked(streamCluster: StreamCluster) { - openStreamBrowseFragment(streamCluster) - } - - override fun onClusterScrolled(streamCluster: StreamCluster) { - viewModel.observeCluster(category, streamCluster) - } - - override fun onAppClick(app: App) { - openDetailsFragment(app.packageName) - } - - override fun onAppLongClick(app: App) { - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/ForceRestartDialog.kt b/app/src/main/java/com/aurora/store/view/ui/commons/ForceRestartDialog.kt deleted file mode 100644 index 01aeed464..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/commons/ForceRestartDialog.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.aurora.store.view.ui.commons - -import android.app.Dialog -import android.os.Bundle -import androidx.fragment.app.DialogFragment -import com.aurora.store.R -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.jakewharton.processphoenix.ProcessPhoenix -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class ForceRestartDialog : DialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.force_restart_title) - .setMessage(R.string.force_restart_summary) - .setPositiveButton(getString(R.string.action_restart)) { _, _ -> - ProcessPhoenix.triggerRebirth(requireContext()) - } - .create() - - override fun onResume() { - super.onResume() - dialog?.setCancelable(false) - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt deleted file mode 100644 index c6809ffc3..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt +++ /dev/null @@ -1,460 +0,0 @@ -package com.aurora.store.view.ui.commons - -import android.app.Dialog -import android.content.Context -import android.os.Bundle -import androidx.annotation.DrawableRes -import androidx.annotation.IdRes -import androidx.annotation.StringRes -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.util.fastForEach -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import coil3.compose.AsyncImage -import coil3.request.ImageRequest -import coil3.request.crossfade -import com.aurora.Constants -import com.aurora.Constants.URL_TOS -import com.aurora.extensions.browse -import com.aurora.extensions.getStyledAttributeColor -import com.aurora.extensions.navigate -import com.aurora.extensions.setAppTheme -import com.aurora.store.MR -import com.aurora.store.R -import com.aurora.store.compose.navigation.Screen -import com.aurora.store.compose.theme.AuroraTheme -import com.aurora.store.util.Preferences -import com.aurora.store.viewmodel.commons.MoreViewModel -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class MoreDialogFragment : DialogFragment() { - - private val viewModel: MoreViewModel by viewModels() - - private var primaryColor: Color = Color.White - private var onPrimaryColor: Color = Color.Black - private var secondaryColor: Color = Color.White - private var onSecondaryColor: Color = Color.Black - - private abstract class Option( - @StringRes open val title: Int, - @DrawableRes open val icon: Int - ) - - private data class ViewOption( - override val title: Int, - override val icon: Int, - @IdRes val destinationID: Int - ) : Option(title, icon) - - private data class ComposeOption( - override val title: Int, - override val icon: Int, - val screen: Screen - ) : Option(title, icon) - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = - MaterialAlertDialogBuilder(requireContext()) - .setView(customDialogView(requireContext())) - .create() - - private fun customDialogView(context: Context): ComposeView = ComposeView(context).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - AuroraTheme { - primaryColor = - Color(requireContext().getStyledAttributeColor(MR.colorSurface)) - onPrimaryColor = - Color(requireContext().getStyledAttributeColor(MR.colorOnSurface)) - secondaryColor = - Color(requireContext().getStyledAttributeColor(MR.colorSecondaryContainer)) - onSecondaryColor = - Color( - requireContext().getStyledAttributeColor(MR.colorOnSecondaryContainer) - ) - - Column( - modifier = Modifier - .fillMaxWidth() - .background(color = primaryColor) - .verticalScroll(rememberScrollState()) - .padding(10.dp), - verticalArrangement = Arrangement.spacedBy( - 4.dp, - Alignment.CenterVertically - ) - ) { - AppBar(onBackgroundColor = onPrimaryColor) - AccountHeader( - backgroundColor = secondaryColor, - onBackgroundColor = onSecondaryColor - ) - Column( - modifier = Modifier - .clip( - RoundedCornerShape( - topStart = 2.dp, - topEnd = 2.dp, - bottomStart = 25.dp, - bottomEnd = 25.dp - ) - ) - .background(color = secondaryColor) - ) { - getOptions().fastForEach { option -> - OptionItem( - option = option, - tintColor = onPrimaryColor, - textColor = onSecondaryColor, - onClick = { - when (option) { - is ViewOption -> { - findNavController().navigate(option.destinationID) - } - - is ComposeOption -> context.navigate(option.screen) - } - } - ) - } - } - getExtraOptions().fastForEach { option -> - OptionItem( - option = option, - tintColor = onPrimaryColor, - textColor = onPrimaryColor, - onClick = { - when (option) { - is ViewOption -> { - findNavController().navigate(option.destinationID) - } - - is ComposeOption -> context.navigate(option.screen) - } - } - ) - } - Footer(onPrimaryColor) - } - } - } - } - - @Composable - fun AppBar(onBackgroundColor: Color) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - .padding(4.dp, 10.dp, 4.dp, 10.dp) - ) { - ThreeStateIconButton(tint = onBackgroundColor) - Text( - modifier = Modifier.wrapContentWidth(), - text = stringResource(id = R.string.app_name), - style = MaterialTheme.typography.titleMedium, - color = onBackgroundColor, - textAlign = TextAlign.Center - ) - - Image( - painter = painterResource(id = R.drawable.ic_cancel), - contentDescription = stringResource(id = R.string.action_cancel), - modifier = Modifier.clickable { - findNavController().navigateUp() - }, - colorFilter = ColorFilter.tint(onBackgroundColor) - ) - } - } - - @Composable - fun Footer(tintColor: Color) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally) - ) { - TextButton(onClick = { requireContext().browse(Constants.URL_POLICY) }) { - Text( - text = stringResource(id = R.string.privacy_policy_title), - fontWeight = FontWeight.Light, - color = tintColor, - fontSize = 12.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - Text(text = "•", color = tintColor) - TextButton(onClick = { requireContext().browse(URL_TOS) }) { - Text( - text = stringResource(id = R.string.menu_terms), - fontWeight = FontWeight.Light, - color = tintColor, - fontSize = 12.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - } - - @Composable - private fun AccountHeader(backgroundColor: Color, onBackgroundColor: Color = Color.White) { - Column( - modifier = Modifier - .fillMaxWidth() - .clip( - RoundedCornerShape( - topStart = 25.dp, - topEnd = 25.dp, - bottomStart = 2.dp, - bottomEnd = 2.dp - ) - ) - .background(color = backgroundColor) - .padding(20.dp), - verticalArrangement = Arrangement.spacedBy(15.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(15.dp, Alignment.Start) - ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data( - if (viewModel.authProvider.isAnonymous) { - R.mipmap.ic_launcher - } else { - viewModel.authProvider.authData?.userProfile?.artwork?.url - } - ) - .crossfade(true) - .build(), - contentDescription = stringResource(id = R.string.title_account_manager), - placeholder = painterResource(R.drawable.ic_account), - contentScale = ContentScale.Crop, - modifier = Modifier - .requiredSize(36.dp) - .clip(CircleShape) - ) - Column( - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.Start - ) { - Text( - text = if (viewModel.authProvider.isAnonymous) { - stringResource(R.string.account_anonymous) - } else { - viewModel.authProvider.authData?.userProfile?.name - ?: stringResource(R.string.status_unavailable) - }, - fontWeight = FontWeight.Normal, - color = onBackgroundColor, - fontSize = 15.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Text( - text = if (viewModel.authProvider.isAnonymous) { - stringResource(R.string.account_anonymous_email) - } else { - viewModel.authProvider.authData?.userProfile?.email - ?: stringResource(R.string.status_unavailable) - }, - fontWeight = FontWeight.Light, - color = onBackgroundColor, - fontSize = 14.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - OutlinedButton( - onClick = { requireContext().navigate(Screen.Accounts) }, - shape = RoundedCornerShape(12.dp), - border = BorderStroke( - 1.dp, - Color( - requireContext().getStyledAttributeColor( - androidx.appcompat.R.attr.colorControlHighlight - ) - ) - ), - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = stringResource(id = R.string.manage_account), - color = onBackgroundColor, - fontWeight = FontWeight.Normal, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - } - - @Composable - private fun OptionItem( - option: Option, - tintColor: Color = Color.Blue, - textColor: Color = Color.Black, - onClick: () -> Unit - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onClick() } - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start) - ) { - Image( - painter = painterResource(id = option.icon), - contentDescription = stringResource(id = option.title), - colorFilter = ColorFilter.tint(color = tintColor), - modifier = Modifier - .padding(horizontal = 10.dp) - .requiredSize(23.dp) - ) - Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = option.title), - color = textColor, - fontSize = 14.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - - @Composable - fun ThreeStateIconButton(tint: Color = Color.White) { - var currentState by remember { - mutableStateOf( - Preferences.getInteger( - requireContext(), - Preferences.PREFERENCE_THEME_STYLE - ).let { - State.entries.getOrNull(it) - } ?: State.Auto - ) - } - - val iconRes = when (currentState) { - State.Light -> R.drawable.ic_light - State.Dark -> R.drawable.ic_dark - else -> R.drawable.ic_auto - } - - Image( - painter = painterResource(id = iconRes), - contentDescription = null, - modifier = Modifier.clickable { - currentState = when (currentState) { - State.Light -> State.Dark - State.Dark -> State.Auto - State.Auto -> State.Light - } - - Preferences.putInteger( - requireContext(), - Preferences.PREFERENCE_THEME_STYLE, - currentState.value - ) - - setAppTheme(currentState.value) - findNavController().navigateUp() - }, - colorFilter = ColorFilter.tint(tint) - ) - } - - enum class State(val value: Int) { - Auto(0), - Light(1), - Dark(2) - } - - private fun getOptions(): List