mirror of
https://github.com/whyorean/AuroraStore.git
synced 2026-02-19 07:15:36 -05:00
Initial migration of home logic to compose
Migrates UpdatesFragment and more dialog to compose. Rest still needs to be adapted. Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
@@ -196,6 +196,7 @@ dependencies {
|
||||
implementation(libs.androidx.adaptive.core)
|
||||
implementation(libs.androidx.adaptive.navigation)
|
||||
implementation(libs.androidx.adaptive.layout)
|
||||
implementation(libs.androidx.adaptive.suite)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.androidx.navigation3.runtime)
|
||||
implementation(libs.androidx.navigation3.ui)
|
||||
|
||||
@@ -30,7 +30,6 @@ 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
|
||||
@@ -42,10 +41,8 @@ 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() {
|
||||
@@ -57,8 +54,7 @@ class MainActivity : AppCompatActivity() {
|
||||
// TopLevelFragments
|
||||
private val topLevelFrags = listOf(
|
||||
R.id.appsContainerFragment,
|
||||
R.id.gamesContainerFragment,
|
||||
R.id.updatesFragment
|
||||
R.id.gamesContainerFragment
|
||||
)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -114,7 +110,6 @@ class MainActivity : AppCompatActivity() {
|
||||
// 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) {
|
||||
@@ -141,16 +136,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||
* 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.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.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.navigation.Screen
|
||||
import com.aurora.store.data.model.ComposeOption
|
||||
import com.aurora.store.data.model.Option
|
||||
|
||||
/**
|
||||
* Composable to display UI navigation options
|
||||
* @param modifier Modifier for the composable
|
||||
* @param option Option to display
|
||||
* @param onClick Callback when this composable is clicked
|
||||
*/
|
||||
@Composable
|
||||
fun OptionListItem(modifier: Modifier = Modifier, option: Option, onClick: () -> Unit = {}) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(dimensionResource(R.dimen.padding_normal)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.margin_small),
|
||||
Alignment.Start
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = option.icon),
|
||||
contentDescription = stringResource(id = option.title),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium))
|
||||
.requiredSize(dimensionResource(R.dimen.icon_size_default))
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(id = option.title),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun OptionListItemPreview() {
|
||||
OptionListItem(
|
||||
option = ComposeOption(
|
||||
title = R.string.title_apps_games,
|
||||
icon = R.drawable.ic_apps,
|
||||
screen = Screen.Installed
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 The Calyx Institute
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.compose.composable
|
||||
|
||||
import android.text.format.Formatter
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.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.PreviewParameter
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.composable.app.AnimatedAppIcon
|
||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||
import com.aurora.store.compose.preview.PreviewTemplate
|
||||
import com.aurora.store.data.room.download.Download
|
||||
import com.aurora.store.data.room.update.Update
|
||||
|
||||
@Composable
|
||||
fun UpdateListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
update: Update,
|
||||
download: Download? = null,
|
||||
onClick: () -> Unit = {},
|
||||
onUpdate: () -> Unit = {},
|
||||
onCancel: () -> Unit = {},
|
||||
isExpanded: Boolean = false
|
||||
) {
|
||||
val isDownloading = download?.isRunning ?: false
|
||||
val size = Formatter.formatShortFileSize(LocalContext.current, update.size)
|
||||
|
||||
var isVisible by remember { mutableStateOf(true) }
|
||||
var isExpanded by remember { mutableStateOf(isExpanded) }
|
||||
|
||||
AnimatedVisibility(visible = isVisible, exit = shrinkVertically() + fadeOut()) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_small)
|
||||
)
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Row(modifier = Modifier.weight(1F)) {
|
||||
AnimatedAppIcon(
|
||||
modifier = Modifier.requiredSize(
|
||||
dimensionResource(R.dimen.icon_size_medium)
|
||||
),
|
||||
iconUrl = update.iconURL,
|
||||
inProgress = download?.isRunning == true,
|
||||
progress = download?.progress?.toFloat() ?: 0F
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = dimensionResource(R.dimen.margin_small))
|
||||
) {
|
||||
Text(
|
||||
text = update.displayName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = "$size • ${update.updatedOn}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.version,
|
||||
update.versionName,
|
||||
update.versionCode
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = { isExpanded = !isExpanded }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_down),
|
||||
contentDescription = stringResource(R.string.expand)
|
||||
)
|
||||
}
|
||||
|
||||
FilledTonalButton(onClick = if (isDownloading) onCancel else onUpdate) {
|
||||
Text(
|
||||
text = when {
|
||||
isDownloading -> stringResource(R.string.action_cancel)
|
||||
else -> stringResource(R.string.action_update)
|
||||
},
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = isExpanded) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = dimensionResource(R.dimen.margin_small))
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_small)))
|
||||
.background(color = MaterialTheme.colorScheme.secondaryContainer)
|
||||
.padding(dimensionResource(R.dimen.padding_medium))
|
||||
) {
|
||||
Text(
|
||||
text = if (update.changelog.isBlank()) {
|
||||
AnnotatedString(
|
||||
text = stringResource(R.string.details_changelog_unavailable)
|
||||
)
|
||||
} else {
|
||||
AnnotatedString.fromHtml(htmlString = update.changelog)
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun UpdateListItemPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||
val context = LocalContext.current
|
||||
PreviewTemplate {
|
||||
UpdateListItem(
|
||||
update = Update.fromApp(context, app),
|
||||
isExpanded = true
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import com.aurora.store.compose.ui.dev.DevProfileScreen
|
||||
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.home.HomeContainerScreen
|
||||
import com.aurora.store.compose.ui.installed.InstalledScreen
|
||||
import com.aurora.store.compose.ui.onboarding.OnboardingScreen
|
||||
import com.aurora.store.compose.ui.preferences.installation.InstallerScreen
|
||||
@@ -155,6 +156,14 @@ fun NavDisplay(startDestination: NavKey) {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
entry<Screen.Home> {
|
||||
HomeContainerScreen(
|
||||
onNavigateTo = { screen ->
|
||||
backstack.add(screen)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,4 +63,7 @@ sealed class Screen : NavKey, Parcelable {
|
||||
|
||||
@Serializable
|
||||
data object Installed : Screen()
|
||||
|
||||
@Serializable
|
||||
data object Home : Screen()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.compose.ui.home
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.aurora.store.compose.preview.PreviewTemplate
|
||||
|
||||
@Composable
|
||||
fun AppsScreen() {
|
||||
ScreenContent()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent() {
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun AppsScreenPreview() {
|
||||
PreviewTemplate {
|
||||
ScreenContent()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.compose.ui.home
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.aurora.store.compose.preview.PreviewTemplate
|
||||
|
||||
@Composable
|
||||
fun GamesScreen() {
|
||||
ScreenContent()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent() {
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun GamesScreenPreview() {
|
||||
PreviewTemplate {
|
||||
ScreenContent()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.compose.ui.home
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.composable.TopAppBar
|
||||
import com.aurora.store.compose.navigation.Screen
|
||||
import com.aurora.store.compose.preview.PreviewTemplate
|
||||
import com.aurora.store.compose.ui.home.menu.HomeContainerMenu
|
||||
import com.aurora.store.compose.ui.home.menu.MenuItem
|
||||
import com.aurora.store.compose.ui.home.navigation.HomeScreen
|
||||
|
||||
@Composable
|
||||
fun HomeContainerScreen(onNavigateTo: (screen: NavKey) -> Unit) {
|
||||
val screens = listOf(
|
||||
HomeScreen.APPS,
|
||||
HomeScreen.GAMES,
|
||||
HomeScreen.UPDATES
|
||||
)
|
||||
|
||||
ScreenContent(
|
||||
screens = screens,
|
||||
onNavigateTo = onNavigateTo
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
default: HomeScreen = HomeScreen.APPS,
|
||||
screens: List<HomeScreen> = emptyList(),
|
||||
onNavigateTo: (screen: NavKey) -> Unit = {}
|
||||
) {
|
||||
var currentScreen by rememberSaveable { mutableStateOf(default) }
|
||||
var shouldShowMoreDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (shouldShowMoreDialog) {
|
||||
MoreDialog(
|
||||
onDismiss = { shouldShowMoreDialog = false },
|
||||
onNavigateTo = onNavigateTo
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SetupMenu() {
|
||||
HomeContainerMenu { menuItem ->
|
||||
when (menuItem) {
|
||||
MenuItem.DOWNLOADS -> onNavigateTo(Screen.Downloads)
|
||||
MenuItem.MORE -> {
|
||||
shouldShowMoreDialog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavigationSuiteScaffold(
|
||||
navigationSuiteItems = {
|
||||
screens.forEach { screen ->
|
||||
item(
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(screen.icon),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(text = stringResource(screen.localized)) },
|
||||
selected = currentScreen == screen,
|
||||
onClick = { currentScreen = screen }
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(currentScreen.localized),
|
||||
actions = { SetupMenu() }
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = { onNavigateTo(Screen.Search) },
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_round_search),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.action_search))
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(modifier = Modifier.padding(paddingValues)) {
|
||||
when (currentScreen) {
|
||||
HomeScreen.APPS -> AppsScreen()
|
||||
HomeScreen.GAMES -> GamesScreen()
|
||||
HomeScreen.UPDATES -> {
|
||||
UpdatesScreen(
|
||||
onNavigateToAppDetails = { packageName ->
|
||||
onNavigateTo(Screen.AppDetails(packageName))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewScreenSizes
|
||||
@Composable
|
||||
private fun HomeContainerScreenPreview() {
|
||||
PreviewTemplate {
|
||||
val screens = listOf(
|
||||
HomeScreen.APPS,
|
||||
HomeScreen.GAMES,
|
||||
HomeScreen.UPDATES
|
||||
)
|
||||
ScreenContent(screens = screens)
|
||||
}
|
||||
}
|
||||
350
app/src/main/java/com/aurora/store/compose/ui/home/MoreDialog.kt
Normal file
350
app/src/main/java/com/aurora/store/compose/ui/home/MoreDialog.kt
Normal file
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.compose.ui.home
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.foundation.background
|
||||
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.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.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.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.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.navigation.NavDeepLinkBuilder
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import com.aurora.Constants.URL_POLICY
|
||||
import com.aurora.Constants.URL_TOS
|
||||
import com.aurora.extensions.browse
|
||||
import com.aurora.extensions.setAppTheme
|
||||
import com.aurora.store.MainActivity
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.composable.OptionListItem
|
||||
import com.aurora.store.compose.navigation.Screen
|
||||
import com.aurora.store.compose.preview.PreviewTemplate
|
||||
import com.aurora.store.data.model.ComposeOption
|
||||
import com.aurora.store.data.model.Option
|
||||
import com.aurora.store.data.model.ThemeState
|
||||
import com.aurora.store.data.model.ViewOption
|
||||
import com.aurora.store.util.Preferences
|
||||
import com.aurora.store.util.Preferences.PREFERENCE_THEME_STYLE
|
||||
|
||||
@Composable
|
||||
fun MoreDialog(
|
||||
avatar: Any = R.mipmap.ic_launcher,
|
||||
name: String = stringResource(R.string.account_anonymous),
|
||||
email: String = stringResource(R.string.account_anonymous_email),
|
||||
onNavigateTo: (screen: NavKey) -> Unit = {},
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
val activity = LocalActivity.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val options = listOf(
|
||||
ComposeOption(
|
||||
title = R.string.title_apps_games,
|
||||
icon = R.drawable.ic_apps,
|
||||
screen = Screen.Installed
|
||||
),
|
||||
ComposeOption(
|
||||
title = R.string.title_blacklist_manager,
|
||||
icon = R.drawable.ic_blacklist,
|
||||
screen = Screen.Blacklist
|
||||
),
|
||||
ComposeOption(
|
||||
title = R.string.title_favourites_manager,
|
||||
icon = R.drawable.ic_favorite_unchecked,
|
||||
screen = Screen.Favourite
|
||||
),
|
||||
ComposeOption(
|
||||
title = R.string.title_spoof_manager,
|
||||
icon = R.drawable.ic_spoof,
|
||||
screen = Screen.Spoof
|
||||
)
|
||||
)
|
||||
val extraOptions = listOf(
|
||||
ViewOption(
|
||||
title = R.string.title_settings,
|
||||
icon = R.drawable.ic_menu_settings,
|
||||
destinationID = R.id.settingsFragment
|
||||
),
|
||||
ComposeOption(
|
||||
title = R.string.title_about,
|
||||
icon = R.drawable.ic_menu_about,
|
||||
screen = Screen.About
|
||||
)
|
||||
)
|
||||
|
||||
fun onOptionClicked(option: Option) {
|
||||
when (option) {
|
||||
is ViewOption -> {
|
||||
val intent = NavDeepLinkBuilder(context)
|
||||
.setGraph(R.navigation.mobile_navigation)
|
||||
.setDestination(option.destinationID)
|
||||
.setComponentName(MainActivity::class.java)
|
||||
.createTaskStackBuilder()
|
||||
.intents
|
||||
.first()
|
||||
|
||||
activity?.startActivity(intent)
|
||||
}
|
||||
|
||||
is ComposeOption -> onNavigateTo(option.screen)
|
||||
}
|
||||
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThemeStateIconButton() {
|
||||
var themeState by remember {
|
||||
mutableStateOf(
|
||||
ThemeState.entries[Preferences.getInteger(context, PREFERENCE_THEME_STYLE)]
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
themeState = when (themeState) {
|
||||
ThemeState.AUTO -> ThemeState.LIGHT
|
||||
ThemeState.LIGHT -> ThemeState.DARK
|
||||
ThemeState.DARK -> ThemeState.AUTO
|
||||
}
|
||||
Preferences.putInteger(context, PREFERENCE_THEME_STYLE, themeState.ordinal)
|
||||
setAppTheme(themeState.ordinal)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = themeState.icon),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppBar() {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
dimensionResource(R.dimen.padding_xsmall),
|
||||
dimensionResource(R.dimen.padding_medium),
|
||||
dimensionResource(R.dimen.padding_xsmall),
|
||||
dimensionResource(R.dimen.padding_medium)
|
||||
)
|
||||
) {
|
||||
ThemeStateIconButton()
|
||||
|
||||
Text(
|
||||
modifier = Modifier.wrapContentWidth(),
|
||||
text = stringResource(id = R.string.app_name),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_cancel),
|
||||
contentDescription = stringResource(id = R.string.action_cancel)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AccountHeader() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
topStart = dimensionResource(R.dimen.radius_large),
|
||||
topEnd = dimensionResource(R.dimen.radius_large),
|
||||
bottomStart = dimensionResource(R.dimen.radius_xxsmall),
|
||||
bottomEnd = dimensionResource(R.dimen.radius_xxsmall)
|
||||
)
|
||||
)
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer)
|
||||
.padding(dimensionResource(R.dimen.padding_large)),
|
||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_large)),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.margin_large),
|
||||
Alignment.Start
|
||||
)
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(avatar)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = stringResource(id = R.string.title_account_manager),
|
||||
placeholder = painterResource(R.drawable.ic_account),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.requiredSize(dimensionResource(R.dimen.icon_size_category))
|
||||
.clip(CircleShape)
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.bodyMediumEmphasized,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = email,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { onNavigateTo(Screen.Accounts) },
|
||||
shape = RoundedCornerShape(dimensionResource(R.dimen.radius_normal)),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_account),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Footer() {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.margin_xxsmall),
|
||||
Alignment.CenterHorizontally
|
||||
)
|
||||
) {
|
||||
TextButton(onClick = { context.browse(URL_POLICY) }) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.privacy_policy_title),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Text(text = "•")
|
||||
TextButton(onClick = { context.browse(URL_TOS) }) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.menu_terms),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(dimensionResource(R.dimen.radius_large))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(dimensionResource(R.dimen.padding_medium)),
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.margin_xsmall),
|
||||
Alignment.CenterVertically
|
||||
)
|
||||
) {
|
||||
AppBar()
|
||||
AccountHeader()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
topStart = dimensionResource(R.dimen.radius_xxsmall),
|
||||
topEnd = dimensionResource(R.dimen.radius_xxsmall),
|
||||
bottomStart = dimensionResource(R.dimen.radius_large),
|
||||
bottomEnd = dimensionResource(R.dimen.radius_large)
|
||||
)
|
||||
)
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer)
|
||||
) {
|
||||
options.fastForEach { option ->
|
||||
OptionListItem(
|
||||
option = option,
|
||||
onClick = { onOptionClicked(option) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extraOptions.fastForEach { option ->
|
||||
OptionListItem(
|
||||
option = option,
|
||||
onClick = { onOptionClicked(option) }
|
||||
)
|
||||
}
|
||||
|
||||
Footer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MoreDialogPreview() {
|
||||
PreviewTemplate {
|
||||
MoreDialog()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.compose.ui.home
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemKey
|
||||
import com.aurora.extensions.emptyPagingItems
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
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.UpdateListItem
|
||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||
import com.aurora.store.compose.preview.PreviewTemplate
|
||||
import com.aurora.store.data.room.update.Update
|
||||
import com.aurora.store.data.room.update.UpdateWithDownload
|
||||
import com.aurora.store.viewmodel.all.UpdatesViewModel
|
||||
import kotlin.random.Random
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
fun UpdatesScreen(
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit,
|
||||
viewModel: UpdatesViewModel = hiltViewModel()
|
||||
) {
|
||||
val hasOngoingUpdates by viewModel.hasOngoingUpdates.collectAsStateWithLifecycle()
|
||||
val isCheckingUpdates by viewModel.isCheckingUpdates.collectAsStateWithLifecycle()
|
||||
val updatesCount by viewModel.updatesCount.collectAsStateWithLifecycle()
|
||||
val updates = viewModel.updates.collectAsLazyPagingItems()
|
||||
|
||||
ScreenContent(
|
||||
updates = updates,
|
||||
totalUpdatesCount = updatesCount,
|
||||
hasOngoingUpdates = hasOngoingUpdates,
|
||||
isCheckingUpdates = isCheckingUpdates,
|
||||
onCheckUpdates = { viewModel.fetchUpdates() },
|
||||
onUpdate = { update -> viewModel.download(update) },
|
||||
onUpdateAll = { viewModel.downloadAll() },
|
||||
onCancel = { packageName -> viewModel.cancelDownload(packageName) },
|
||||
onCancelAll = { viewModel.cancelAll() },
|
||||
onNavigateToAppDetails = onNavigateToAppDetails
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
hasOngoingUpdates: Boolean = false,
|
||||
totalUpdatesCount: Int = 0,
|
||||
updates: LazyPagingItems<UpdateWithDownload> = emptyPagingItems(),
|
||||
isCheckingUpdates: Boolean = false,
|
||||
onCheckUpdates: () -> Unit = {},
|
||||
onUpdate: (update: Update) -> Unit = {},
|
||||
onUpdateAll: () -> Unit = {},
|
||||
onCancel: (packageName: String) -> Unit = {},
|
||||
onCancelAll: () -> Unit = {},
|
||||
onNavigateToAppDetails: (packageName: String) -> Unit = {}
|
||||
) {
|
||||
/*
|
||||
* For some reason paging3 frequently out-of-nowhere invalidates the list which causes
|
||||
* the loading animation to play again even if the keys are same causing a glitching effect.
|
||||
*
|
||||
* Save the initial loading state to make sure we don't replay the loading animation again.
|
||||
*/
|
||||
var initialLoad by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
val state = rememberPullToRefreshState()
|
||||
|
||||
PullToRefreshBox(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
isRefreshing = isCheckingUpdates,
|
||||
onRefresh = onCheckUpdates,
|
||||
state = state,
|
||||
indicator = {
|
||||
PullToRefreshDefaults.LoadingIndicator(
|
||||
state = state,
|
||||
isRefreshing = isCheckingUpdates,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
}
|
||||
) {
|
||||
when {
|
||||
updates.loadState.refresh is LoadState.Loading && initialLoad -> {
|
||||
ContainedLoadingIndicator()
|
||||
}
|
||||
|
||||
else -> {
|
||||
initialLoad = false
|
||||
|
||||
if (updates.itemCount == 0) {
|
||||
Error(
|
||||
painter = painterResource(R.drawable.ic_updates),
|
||||
message = stringResource(R.string.details_no_updates),
|
||||
actionMessage = stringResource(R.string.check_updates),
|
||||
onAction = onCheckUpdates
|
||||
)
|
||||
} else {
|
||||
LazyColumn {
|
||||
stickyHeader {
|
||||
Surface(modifier = Modifier.fillMaxWidth()) {
|
||||
Header(
|
||||
hasOngoingUpdates = hasOngoingUpdates,
|
||||
totalUpdatesCount = totalUpdatesCount,
|
||||
onUpdateAll = onUpdateAll,
|
||||
onCancelAll = onCancelAll
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
count = updates.itemCount,
|
||||
key = updates.itemKey { it.update.packageName }
|
||||
) { index ->
|
||||
updates[index]?.let { updateWithDownload ->
|
||||
val update = updateWithDownload.update
|
||||
UpdateListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
update = update,
|
||||
download = updateWithDownload.download,
|
||||
onUpdate = { onUpdate(update) },
|
||||
onCancel = { onCancel(update.packageName) },
|
||||
onClick = { onNavigateToAppDetails(update.packageName) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(
|
||||
modifier: Modifier = Modifier,
|
||||
totalUpdatesCount: Int = 1,
|
||||
onUpdateAll: () -> Unit = {},
|
||||
onCancelAll: () -> Unit = {},
|
||||
hasOngoingUpdates: Boolean = false
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_xsmall)
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = when (totalUpdatesCount) {
|
||||
1 -> "$totalUpdatesCount ${stringResource(R.string.update_available)}"
|
||||
else -> "$totalUpdatesCount ${stringResource(R.string.updates_available)}"
|
||||
},
|
||||
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Button(onClick = if (hasOngoingUpdates) onCancelAll else onUpdateAll) {
|
||||
Text(
|
||||
text = when {
|
||||
hasOngoingUpdates -> stringResource(R.string.action_cancel)
|
||||
else -> stringResource(R.string.action_update_all)
|
||||
},
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun UpdatesScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||
val context = LocalContext.current
|
||||
PreviewTemplate {
|
||||
val updates = List(10) {
|
||||
UpdateWithDownload(
|
||||
update = Update.fromApp(context, app)
|
||||
.copy(packageName = Random.nextInt().toString()),
|
||||
download = null
|
||||
)
|
||||
}
|
||||
val pagedUpdates = MutableStateFlow(PagingData.from(updates)).collectAsLazyPagingItems()
|
||||
ScreenContent(
|
||||
isCheckingUpdates = true,
|
||||
updates = pagedUpdates
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.compose.ui.home.menu
|
||||
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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 com.aurora.store.R
|
||||
import com.aurora.store.compose.preview.PreviewTemplate
|
||||
|
||||
/**
|
||||
* Menu for home container screen
|
||||
* @param modifier Modifier for the composable
|
||||
* @param onMenuItemClicked Callback when a menu action is clicked
|
||||
*/
|
||||
@Composable
|
||||
fun HomeContainerMenu(
|
||||
modifier: Modifier = Modifier,
|
||||
onMenuItemClicked: (menuItem: MenuItem) -> Unit = {}
|
||||
) {
|
||||
IconButton(onClick = { onMenuItemClicked(MenuItem.DOWNLOADS) }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_download_manager),
|
||||
contentDescription = stringResource(R.string.title_download_manager)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { onMenuItemClicked(MenuItem.MORE) }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_settings_account),
|
||||
contentDescription = stringResource(R.string.title_more)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun HomeContainerMenuPreview() {
|
||||
PreviewTemplate {
|
||||
HomeContainerMenu()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.compose.ui.home.menu
|
||||
|
||||
/**
|
||||
* Valid actions for home menu
|
||||
*/
|
||||
enum class MenuItem {
|
||||
DOWNLOADS,
|
||||
MORE
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.compose.ui.home.navigation
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.aurora.store.R
|
||||
|
||||
/**
|
||||
* Screens that can be shown in home
|
||||
*/
|
||||
enum class HomeScreen(@StringRes val localized: Int, @DrawableRes val icon: 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)
|
||||
}
|
||||
@@ -90,11 +90,17 @@ class UpdateHelper @Inject constructor(
|
||||
.map { list -> if (!isExtendedUpdateEnabled) list.filter { it.hasValidCert } else list }
|
||||
.stateIn(AuroraApp.scope, SharingStarted.WhileSubscribed(), null)
|
||||
|
||||
val pagedUpdates get() = updateDao.pagedUpdates()
|
||||
|
||||
val isCheckingUpdates = WorkManager.getInstance(context)
|
||||
.getWorkInfosForUniqueWorkFlow(EXPEDITED_UPDATE_WORKER)
|
||||
.map { list -> !list.all { it.state.isFinished } }
|
||||
.stateIn(AuroraApp.scope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
val hasOngoingUpdates get() = updateDao.hasOngoingUpdates()
|
||||
|
||||
val updatesCount get() = updateDao.updatesCount()
|
||||
|
||||
/**
|
||||
* Deletes invalid updates from database and starts observing events
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,6 @@ enum class DownloadStatus(@StringRes val localized: Int) {
|
||||
|
||||
companion object {
|
||||
val finished = listOf(FAILED, CANCELLED, COMPLETED)
|
||||
val running = listOf(QUEUED, PURCHASING, DOWNLOADING)
|
||||
val running = listOf(QUEUED, PURCHASING, DOWNLOADING, VERIFYING)
|
||||
}
|
||||
}
|
||||
|
||||
28
app/src/main/java/com/aurora/store/data/model/Option.kt
Normal file
28
app/src/main/java/com/aurora/store/data/model/Option.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.data.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.aurora.store.compose.navigation.Screen
|
||||
|
||||
abstract class Option(
|
||||
@StringRes open val title: Int,
|
||||
@DrawableRes open val icon: Int
|
||||
)
|
||||
|
||||
data class ViewOption(
|
||||
override val title: Int,
|
||||
override val icon: Int,
|
||||
@IdRes val destinationID: Int
|
||||
) : Option(title, icon)
|
||||
|
||||
data class ComposeOption(
|
||||
override val title: Int,
|
||||
override val icon: Int,
|
||||
val screen: Screen
|
||||
) : Option(title, icon)
|
||||
18
app/src/main/java/com/aurora/store/data/model/ThemeState.kt
Normal file
18
app/src/main/java/com/aurora/store/data/model/ThemeState.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.data.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.aurora.store.R
|
||||
|
||||
/**
|
||||
* Values for supported theme states
|
||||
*/
|
||||
enum class ThemeState(@DrawableRes val icon: Int) {
|
||||
AUTO(R.drawable.ic_auto),
|
||||
LIGHT(R.drawable.ic_light),
|
||||
DARK(R.drawable.ic_dark)
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.aurora.store.data.room.update
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import com.aurora.store.data.model.DownloadStatus
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
@@ -27,4 +29,28 @@ interface UpdateDao {
|
||||
deleteAll()
|
||||
insertAll(updates)
|
||||
}
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM `update`
|
||||
LEFT JOIN download ON `update`.packageName = `download`.packageName
|
||||
ORDER BY `update`.displayName ASC
|
||||
"""
|
||||
)
|
||||
fun pagedUpdates(): PagingSource<Int, UpdateWithDownload>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM `update`
|
||||
INNER JOIN download ON `update`.packageName = `download`.packageName
|
||||
WHERE `download`.downloadStatus IN (:status)
|
||||
)
|
||||
"""
|
||||
)
|
||||
fun hasOngoingUpdates(status: List<DownloadStatus> = DownloadStatus.running): Flow<Boolean>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM `update`")
|
||||
fun updatesCount(): Flow<Int>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 The Calyx Institute
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.data.room.update
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
import com.aurora.store.data.room.download.Download
|
||||
|
||||
data class UpdateWithDownload(
|
||||
@Embedded val update: Update,
|
||||
@Relation(parentColumn = "packageName", entityColumn = "packageName") val download: Download?
|
||||
)
|
||||
@@ -217,7 +217,7 @@ object NotificationUtil {
|
||||
.setGraph(R.navigation.mobile_navigation)
|
||||
.setDestination(R.id.splashFragment)
|
||||
.setComponentName(MainActivity::class.java)
|
||||
.setArguments(bundleOf("destinationId" to R.id.updatesFragment))
|
||||
.setArguments(bundleOf("destinationId" to 2))
|
||||
.createPendingIntent()
|
||||
|
||||
return NotificationCompat.Builder(context, Constants.NOTIFICATION_CHANNEL_UPDATES)
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Aurora Store
|
||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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<ViewHeaderUpdateBinding>(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
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
/*
|
||||
* Aurora Store
|
||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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<ViewAppUpdateBinding>(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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,21 +172,9 @@ abstract class BaseFlavouredSplashFragment : BaseFragment<FragmentSplashBinding>
|
||||
private fun navigateToDefaultTab() {
|
||||
val defaultDestination =
|
||||
Preferences.getInteger(requireContext(), PREFERENCE_DEFAULT_SELECTED_TAB)
|
||||
val directions =
|
||||
when (requireArguments().getInt("destinationId", defaultDestination)) {
|
||||
R.id.updatesFragment -> {
|
||||
requireArguments().remove("destinationId")
|
||||
SplashFragmentDirections.actionSplashFragmentToUpdatesFragment()
|
||||
}
|
||||
|
||||
1 -> SplashFragmentDirections.actionSplashFragmentToGamesContainerFragment()
|
||||
|
||||
2 -> SplashFragmentDirections.actionSplashFragmentToUpdatesFragment()
|
||||
|
||||
else -> SplashFragmentDirections.actionSplashFragmentToNavigationApps()
|
||||
}
|
||||
// TODO: Handle default destination
|
||||
requireActivity().viewModelStore.clear() // Clear ViewModelStore to avoid bugs with logout
|
||||
findNavController().navigate(directions)
|
||||
requireContext().navigate(Screen.Home)
|
||||
}
|
||||
|
||||
private fun requestAuthTokenForGoogle(accountName: String, oldToken: String? = null) {
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
/*
|
||||
* Aurora Store
|
||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.aurora.store.view.ui.updates
|
||||
|
||||
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.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.aurora.Constants
|
||||
import com.aurora.extensions.browse
|
||||
import com.aurora.extensions.navigate
|
||||
import com.aurora.extensions.requiresObbDir
|
||||
import com.aurora.store.MobileNavigationDirections
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.navigation.Screen
|
||||
import com.aurora.store.data.model.MinimalApp
|
||||
import com.aurora.store.data.model.PermissionType
|
||||
import com.aurora.store.data.providers.PermissionProvider.Companion.isGranted
|
||||
import com.aurora.store.data.room.download.Download
|
||||
import com.aurora.store.data.room.update.Update
|
||||
import com.aurora.store.databinding.FragmentUpdatesBinding
|
||||
import com.aurora.store.view.epoxy.views.UpdateHeaderViewModel_
|
||||
import com.aurora.store.view.epoxy.views.app.AppUpdateViewModel_
|
||||
import com.aurora.store.view.epoxy.views.app.NoAppViewModel_
|
||||
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
|
||||
import com.aurora.store.view.ui.commons.BaseFragment
|
||||
import com.aurora.store.viewmodel.all.UpdatesViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class UpdatesFragment : BaseFragment<FragmentUpdatesBinding>() {
|
||||
|
||||
private val viewModel: UpdatesViewModel 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<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = insets.bottom + resources.getDimensionPixelSize(R.dimen.margin_large)
|
||||
}
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
// Toolbar
|
||||
binding.toolbar.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_download_manager -> {
|
||||
requireContext().navigate(Screen.Downloads)
|
||||
}
|
||||
|
||||
R.id.menu_more -> {
|
||||
findNavController().navigate(
|
||||
MobileNavigationDirections.actionGlobalMoreDialogFragment()
|
||||
)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.updates
|
||||
.combine(viewModel.downloadsList) { uList, dList ->
|
||||
uList?.associateWith { a ->
|
||||
dList.find {
|
||||
it.packageName == a.packageName && it.versionCode == a.versionCode
|
||||
}
|
||||
}
|
||||
}.collectLatest { map ->
|
||||
updateController(map)
|
||||
viewModel.updateAllEnqueued = map?.values?.all {
|
||||
it?.isRunning == true
|
||||
} ?: false
|
||||
}
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.fetchingUpdates.collect {
|
||||
binding.swipeRefreshLayout.isRefreshing = it
|
||||
if (it && viewModel.updates.value.isNullOrEmpty()) {
|
||||
updateController(emptyMap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
viewModel.fetchUpdates()
|
||||
}
|
||||
|
||||
binding.searchFab.setOnClickListener {
|
||||
requireContext().navigate(Screen.Search)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateController(appList: Map<Update, Download?>?) {
|
||||
binding.recycler.withModels {
|
||||
setFilterDuplicates(true)
|
||||
if (appList == null) {
|
||||
for (i in 1..10) {
|
||||
add(
|
||||
AppListViewShimmerModel_()
|
||||
.id(i)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (appList.isEmpty()) {
|
||||
add(
|
||||
NoAppViewModel_()
|
||||
.id("no_update")
|
||||
.icon(R.drawable.ic_updates)
|
||||
.message(R.string.details_no_updates)
|
||||
.showAction(true)
|
||||
.actionMessage(R.string.check_updates)
|
||||
.actionCallback { _ -> viewModel.fetchUpdates() }
|
||||
)
|
||||
} else {
|
||||
add(
|
||||
UpdateHeaderViewModel_()
|
||||
.id("header_all")
|
||||
.title(
|
||||
"${appList.size} " +
|
||||
if (appList.size == 1) {
|
||||
getString(R.string.update_available)
|
||||
} else {
|
||||
getString(R.string.updates_available)
|
||||
}
|
||||
)
|
||||
.action(
|
||||
if (viewModel.updateAllEnqueued) {
|
||||
getString(R.string.action_cancel)
|
||||
} else {
|
||||
getString(R.string.action_update_all)
|
||||
}
|
||||
)
|
||||
.click { _ ->
|
||||
if (viewModel.updateAllEnqueued) {
|
||||
cancelAll()
|
||||
} else {
|
||||
updateAll()
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
)
|
||||
|
||||
for ((update, download) in appList) {
|
||||
add(
|
||||
AppUpdateViewModel_()
|
||||
.id(update.packageName)
|
||||
.update(update)
|
||||
.download(download)
|
||||
.click { _ ->
|
||||
if (update.packageName == requireContext().packageName) {
|
||||
requireContext().browse(Constants.GITLAB_URL)
|
||||
} else {
|
||||
openDetailsFragment(update.packageName)
|
||||
}
|
||||
}
|
||||
.longClick { _ ->
|
||||
openAppMenuSheet(MinimalApp.fromUpdate(update))
|
||||
false
|
||||
}
|
||||
.positiveAction { _ -> updateSingle(update) }
|
||||
.negativeAction { _ -> cancelSingle(update) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSingle(update: Update) {
|
||||
if (update.fileList.requiresObbDir()) {
|
||||
if (isGranted(requireContext(), PermissionType.STORAGE_MANAGER)) {
|
||||
viewModel.download(update)
|
||||
} else {
|
||||
permissionProvider.request(PermissionType.STORAGE_MANAGER) {
|
||||
if (it) viewModel.download(update)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
viewModel.download(update)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAll() {
|
||||
viewModel.updateAllEnqueued = true
|
||||
if (viewModel.updates.value?.any { it.fileList.requiresObbDir() } == true) {
|
||||
if (isGranted(requireContext(), PermissionType.STORAGE_MANAGER)) {
|
||||
viewModel.downloadAll()
|
||||
} else {
|
||||
permissionProvider.request(PermissionType.STORAGE_MANAGER) {
|
||||
if (it) viewModel.downloadAll()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
viewModel.downloadAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelSingle(update: Update) {
|
||||
viewModel.cancelDownload(update.packageName)
|
||||
}
|
||||
|
||||
private fun cancelAll() {
|
||||
viewModel.cancelAll()
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,22 @@ package com.aurora.store.viewmodel.all
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import com.aurora.store.data.helper.DownloadHelper
|
||||
import com.aurora.store.data.helper.UpdateHelper
|
||||
import com.aurora.store.data.paging.GenericPagingSource.Companion.pager
|
||||
import com.aurora.store.data.room.update.Update
|
||||
import com.aurora.store.data.room.update.UpdateWithDownload
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
@@ -34,12 +45,19 @@ class UpdatesViewModel @Inject constructor(
|
||||
private val downloadHelper: DownloadHelper
|
||||
) : ViewModel() {
|
||||
|
||||
var updateAllEnqueued: Boolean = false
|
||||
private val _updates = MutableStateFlow<PagingData<UpdateWithDownload>>(PagingData.empty())
|
||||
val updates = _updates.asStateFlow()
|
||||
|
||||
val downloadsList get() = downloadHelper.downloadsList
|
||||
val updates get() = updateHelper.updates
|
||||
val isCheckingUpdates = updateHelper.isCheckingUpdates
|
||||
val hasOngoingUpdates = updateHelper.hasOngoingUpdates
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||
|
||||
val fetchingUpdates = updateHelper.isCheckingUpdates
|
||||
val updatesCount = updateHelper.updatesCount
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
|
||||
|
||||
init {
|
||||
getPagedUpdates()
|
||||
}
|
||||
|
||||
fun fetchUpdates() {
|
||||
updateHelper.checkUpdatesNow()
|
||||
@@ -51,7 +69,7 @@ class UpdatesViewModel @Inject constructor(
|
||||
|
||||
fun downloadAll() {
|
||||
viewModelScope.launch {
|
||||
updates.value?.forEach { downloadHelper.enqueueUpdate(it) }
|
||||
updateHelper.updates.value?.forEach { downloadHelper.enqueueUpdate(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,4 +80,12 @@ class UpdatesViewModel @Inject constructor(
|
||||
fun cancelAll() {
|
||||
viewModelScope.launch { downloadHelper.cancelAll(true) }
|
||||
}
|
||||
|
||||
private fun getPagedUpdates() {
|
||||
pager { updateHelper.pagedUpdates }.flow
|
||||
.distinctUntilChanged()
|
||||
.cachedIn(viewModelScope)
|
||||
.onEach { _updates.value = it }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
~ Aurora Store
|
||||
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
||||
~
|
||||
~ 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 <http://www.gnu.org/licenses/>.
|
||||
~
|
||||
-->
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/layout_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="@dimen/padding_medium"
|
||||
android:paddingTop="@dimen/padding_small"
|
||||
android:paddingEnd="@dimen/padding_small"
|
||||
android:paddingBottom="@dimen/padding_small">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/head_flipper"
|
||||
android:layout_width="@dimen/icon_size_medium"
|
||||
android:layout_height="@dimen/icon_size_medium">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/img_icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerInParent="true" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progress_download"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerInParent="true"
|
||||
android:visibility="gone"
|
||||
app:indicatorSize="@dimen/icon_size_medium"
|
||||
app:trackThickness="3dp"
|
||||
tools:progress="40" />
|
||||
</RelativeLayout>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/txt_line1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_normal"
|
||||
android:layout_toStartOf="@+id/headerIndicator"
|
||||
android:layout_toEndOf="@id/head_flipper"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.Aurora.Line1" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/txt_line2"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/txt_line1"
|
||||
android:layout_alignStart="@+id/txt_line1"
|
||||
android:layout_alignEnd="@id/txt_line1"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.Aurora.Line3" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/txt_line3"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/txt_line2"
|
||||
android:layout_alignStart="@+id/txt_line1"
|
||||
android:layout_alignEnd="@id/txt_line1"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.Aurora.Line3" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/txt_line4"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/txt_line3"
|
||||
android:layout_alignStart="@+id/txt_line1"
|
||||
android:layout_alignEnd="@id/txt_line1"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.Aurora.Line3" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/headerIndicator"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@id/txt_line1"
|
||||
android:layout_toStartOf="@id/btn_action"
|
||||
android:contentDescription="@string/details_changelog"
|
||||
app:iconTint="?colorControlNormal"
|
||||
app:icon="@drawable/ic_arrow_down" />
|
||||
|
||||
<com.aurora.store.view.custom.layouts.button.UpdateButton
|
||||
android:id="@+id/btn_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/height_button"
|
||||
android:layout_alignTop="@id/txt_line1"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:gravity="center" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_changelog"
|
||||
style="@style/Widget.Material3.CardView.Filled"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/txt_line4"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:visibility="gone"
|
||||
app:contentPadding="@dimen/padding_small">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/txt_changelog"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="false"
|
||||
android:textAppearance="@style/TextAppearance.Aurora.Line2"
|
||||
android:textIsSelectable="true" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</RelativeLayout>
|
||||
@@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Aurora Store
|
||||
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
||||
~
|
||||
~ 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 <http://www.gnu.org/licenses/>.
|
||||
~
|
||||
-->
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/padding_medium"
|
||||
android:paddingTop="@dimen/padding_xsmall"
|
||||
android:paddingEnd="@dimen/padding_small"
|
||||
android:paddingBottom="@dimen/padding_xsmall">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txt_title"
|
||||
style="@style/AuroraTextStyle.Subtitle.Alt"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/btn_action"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
tools:text="Header" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:ellipsize="end"
|
||||
android:text="@string/action_update_all" />
|
||||
</RelativeLayout>
|
||||
@@ -29,9 +29,4 @@
|
||||
android:icon="@drawable/ic_games"
|
||||
android:title="@string/title_games" />
|
||||
|
||||
<item
|
||||
android:id="@id/updatesFragment"
|
||||
android:icon="@drawable/ic_updates"
|
||||
android:title="@string/title_updates" />
|
||||
|
||||
</menu>
|
||||
</menu>
|
||||
|
||||
@@ -51,11 +51,6 @@
|
||||
android:name="com.aurora.store.view.ui.games.GamesContainerFragment"
|
||||
android:label="@string/title_games"
|
||||
tools:layout="@layout/fragment_apps_games" />
|
||||
<fragment
|
||||
android:id="@+id/updatesFragment"
|
||||
android:name="com.aurora.store.view.ui.updates.UpdatesFragment"
|
||||
android:label="@string/title_updates"
|
||||
tools:layout="@layout/fragment_updates" />
|
||||
<fragment
|
||||
android:id="@+id/settingsFragment"
|
||||
android:name="com.aurora.store.view.ui.preferences.SettingsFragment"
|
||||
@@ -162,12 +157,6 @@
|
||||
app:launchSingleTop="true"
|
||||
app:popUpTo="@id/mobile_navigation"
|
||||
app:popUpToInclusive="true" />
|
||||
<action
|
||||
android:id="@+id/action_splashFragment_to_updatesFragment"
|
||||
app:destination="@id/updatesFragment"
|
||||
app:launchSingleTop="true"
|
||||
app:popUpTo="@id/mobile_navigation"
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/googleFragment"
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
|
||||
<dimen name="review_height">200dp</dimen>
|
||||
|
||||
<dimen name="radius_xxsmall">2dp</dimen>
|
||||
<dimen name="radius_small">8dp</dimen>
|
||||
<dimen name="radius_medium">10dp</dimen>
|
||||
<dimen name="radius_normal">12dp</dimen>
|
||||
|
||||
@@ -58,6 +58,7 @@ androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core" }
|
||||
androidx-adaptive-core = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptive" }
|
||||
androidx-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "adaptive" }
|
||||
androidx-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "adaptive" }
|
||||
androidx-adaptive-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "adaptive" }
|
||||
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
|
||||
androidx-hilt-viewmodel = { module = "androidx.hilt:hilt-lifecycle-viewmodel-compose", version.ref = "androidx-hilt" }
|
||||
androidx-junit = { module = "androidx.test.ext:junit-ktx", version.ref = "androidx-junit" }
|
||||
|
||||
Reference in New Issue
Block a user