feat: move update to notification card (#1917)

This commit is contained in:
Robert 2024-05-27 21:50:02 +02:00 committed by GitHub
parent 4e7d96e91d
commit 6bfd9098d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 222 additions and 265 deletions

View file

@ -5,16 +5,8 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.destination.SettingsDestination import app.revanced.manager.ui.destination.SettingsDestination
import app.revanced.manager.ui.screen.AppSelectorScreen import app.revanced.manager.ui.screen.AppSelectorScreen
@ -46,7 +38,6 @@ class MainActivity : ComponentActivity() {
installSplashScreen() installSplashScreen()
val vm: MainViewModel = getAndroidViewModel() val vm: MainViewModel = getAndroidViewModel()
vm.importLegacySettings(this) vm.importLegacySettings(this)
setContent { setContent {
@ -59,37 +50,8 @@ class MainActivity : ComponentActivity() {
) { ) {
val navController = val navController =
rememberNavController<Destination>(startDestination = Destination.Dashboard) rememberNavController<Destination>(startDestination = Destination.Dashboard)
NavBackHandler(navController) NavBackHandler(navController)
val firstLaunch by vm.prefs.firstLaunch.getAsState()
if (firstLaunch) AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
vm.updatedManagerVersion?.let {
AlertDialog(
onDismissRequest = vm::dismissUpdateDialog,
confirmButton = {
TextButton(
onClick = {
vm.dismissUpdateDialog()
navController.navigate(Destination.Settings(SettingsDestination.Update(false)))
}
) {
Text(stringResource(R.string.update))
}
},
dismissButton = {
TextButton(onClick = vm::dismissUpdateDialog) {
Text(stringResource(R.string.dismiss_temporary))
}
},
icon = { Icon(Icons.Outlined.Update, null) },
title = { Text(stringResource(R.string.update_available_dialog_title)) },
text = { Text(stringResource(R.string.update_available_dialog_description, it)) }
)
}
AnimatedNavHost( AnimatedNavHost(
controller = navController controller = navController
) { destination -> ) { destination ->
@ -97,6 +59,9 @@ class MainActivity : ComponentActivity() {
is Destination.Dashboard -> DashboardScreen( is Destination.Dashboard -> DashboardScreen(
onSettingsClick = { navController.navigate(Destination.Settings()) }, onSettingsClick = { navController.navigate(Destination.Settings()) },
onAppSelectorClick = { navController.navigate(Destination.AppSelector) }, onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
onUpdateClick = { navController.navigate(
Destination.Settings(SettingsDestination.Update())
) },
onAppClick = { installedApp -> onAppClick = { installedApp ->
navController.navigate( navController.navigate(
Destination.InstalledApplicationInfo( Destination.InstalledApplicationInfo(

View file

@ -4,7 +4,6 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Source import androidx.compose.material.icons.outlined.Source
import androidx.compose.material.icons.outlined.Update import androidx.compose.material.icons.outlined.Update
@ -13,7 +12,6 @@ import androidx.compose.material3.Checkbox
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -21,11 +19,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
@ -37,32 +33,19 @@ fun AutoUpdatesDialog(onSubmit: (Boolean, Boolean) -> Unit) {
AlertDialog( AlertDialog(
onDismissRequest = {}, onDismissRequest = {},
confirmButton = { confirmButton = {
TextButton( TextButton(onClick = { onSubmit(managerEnabled, patchesEnabled) }) {
onClick = { onSubmit(managerEnabled, patchesEnabled) }
) {
Text(stringResource(R.string.save)) Text(stringResource(R.string.save))
} }
}, },
icon = { icon = { Icon(Icons.Outlined.Update, null) },
Icon(Icons.Outlined.Update, null) title = { Text(text = stringResource(R.string.auto_updates_dialog_title)) },
},
title = {
Text(
text = stringResource(R.string.auto_updates_dialog_title),
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
color = MaterialTheme.colorScheme.onSurface,
)
},
text = { text = {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
Text( Text(text = stringResource(R.string.auto_updates_dialog_description))
text = stringResource(R.string.auto_updates_dialog_description),
style = MaterialTheme.typography.bodyMedium,
)
Column {
AutoUpdatesItem( AutoUpdatesItem(
headline = R.string.auto_updates_dialog_manager, headline = R.string.auto_updates_dialog_manager,
icon = Icons.Outlined.Update, icon = Icons.Outlined.Update,
@ -76,13 +59,9 @@ fun AutoUpdatesDialog(onSubmit: (Boolean, Boolean) -> Unit) {
checked = patchesEnabled, checked = patchesEnabled,
onCheckedChange = { patchesEnabled = it } onCheckedChange = { patchesEnabled = it }
) )
}
Text( Text(text = stringResource(R.string.auto_updates_dialog_note))
text = stringResource(R.string.auto_updates_dialog_note),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.fillMaxWidth()
)
} }
} }
) )
@ -94,22 +73,9 @@ private fun AutoUpdatesItem(
icon: ImageVector, icon: ImageVector,
checked: Boolean, checked: Boolean,
onCheckedChange: (Boolean) -> Unit onCheckedChange: (Boolean) -> Unit
) { ) = ListItem(
ListItem( leadingContent = { Icon(icon, null) },
leadingContent = { Icon(icon, null, tint = MaterialTheme.colorScheme.onSurface) }, headlineContent = { Text(stringResource(headline)) },
headlineContent = { trailingContent = { Checkbox(checked = checked, onCheckedChange = null) },
Text(
text = stringResource(headline),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
},
trailingContent = {
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
},
modifier = Modifier.clickable { onCheckedChange(!checked) } modifier = Modifier.clickable { onCheckedChange(!checked) }
) )
}

View file

@ -1,8 +1,10 @@
package app.revanced.manager.ui.component package app.revanced.manager.ui.component
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -11,7 +13,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -27,87 +28,91 @@ import app.revanced.manager.R
@Composable @Composable
fun NotificationCard( fun NotificationCard(
isWarning: Boolean = false,
title: String? = null,
text: String, text: String,
icon: ImageVector, icon: ImageVector,
actions: (@Composable () -> Unit)? modifier: Modifier = Modifier,
actions: (@Composable RowScope.() -> Unit)? = null,
title: String? = null,
isWarning: Boolean = false
) { ) {
val color = val color =
if (isWarning) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.onPrimaryContainer if (isWarning) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.onPrimaryContainer
NotificationCardInstance(isWarning = isWarning) { NotificationCardInstance(modifier = modifier, isWarning = isWarning) {
Column( Row(
modifier = Modifier.padding(if (title != null) 20.dp else 16.dp), modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
if (title != null) { Box(
Icon( modifier = Modifier.size(28.dp),
modifier = Modifier.size(36.dp), contentAlignment = Alignment.Center
imageVector = icon,
contentDescription = null,
tint = color,
)
Column(
verticalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = color,
)
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
color = color,
)
}
} else {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Icon( Icon(
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
tint = color, tint = color,
) )
}
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
title?.let {
Text(
text = it,
style = MaterialTheme.typography.titleLarge,
color = color,
)
}
Text( Text(
text = text, text = text,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = color, color = color,
) )
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
) {
actions?.invoke(this)
} }
} }
actions?.invoke()
} }
} }
} }
@Composable @Composable
fun NotificationCard( fun NotificationCard(
isWarning: Boolean = false,
title: String? = null,
text: String, text: String,
icon: ImageVector, icon: ImageVector,
modifier: Modifier = Modifier,
title: String? = null,
isWarning: Boolean = false,
onDismiss: (() -> Unit)? = null, onDismiss: (() -> Unit)? = null,
primaryAction: (() -> Unit)? = null onClick: (() -> Unit)? = null
) { ) {
val color = val color =
if (isWarning) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.onPrimaryContainer if (isWarning) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.onPrimaryContainer
NotificationCardInstance(isWarning = isWarning, onClick = primaryAction) { NotificationCardInstance(modifier = modifier, isWarning = isWarning, onClick = onClick) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Box(
modifier = Modifier.size(28.dp),
contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
modifier = Modifier.size(if (title != null) 36.dp else 24.dp), modifier = Modifier.size(24.dp),
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
tint = color, tint = color,
) )
}
if (title != null) { if (title != null) {
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@ -145,32 +150,31 @@ fun NotificationCard(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun NotificationCardInstance( private fun NotificationCardInstance(
modifier: Modifier = Modifier,
isWarning: Boolean = false, isWarning: Boolean = false,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val colors = val colors =
CardDefaults.cardColors(containerColor = if (isWarning) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primaryContainer) CardDefaults.cardColors(containerColor = if (isWarning) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primaryContainer)
val modifier = Modifier val defaultModifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp)
.clip(RoundedCornerShape(24.dp)) .clip(RoundedCornerShape(24.dp))
if (onClick != null) { if (onClick != null) {
Card( Card(
onClick = onClick, onClick = onClick,
colors = colors, colors = colors,
modifier = modifier modifier = modifier.then(defaultModifier)
) { ) {
content() content()
} }
} else { } else {
Card( Card(
colors = colors, colors = colors,
modifier = modifier, modifier = modifier.then(defaultModifier)
) { ) {
content() content()
} }

View file

@ -65,34 +65,22 @@ fun BundleItem(
.height(64.dp) .height(64.dp)
.fillMaxWidth() .fillMaxWidth()
.combinedClickable( .combinedClickable(
onClick = { onClick = { viewBundleDialogPage = true },
viewBundleDialogPage = true
},
onLongClick = onSelect, onLongClick = onSelect,
), ),
leadingContent = { leadingContent = if (selectable) {
if(selectable) { {
Checkbox( Checkbox(
checked = isBundleSelected, checked = isBundleSelected,
onCheckedChange = toggleSelection, onCheckedChange = toggleSelection,
) )
} }
}, } else null,
headlineContent = { headlineContent = { Text(text = bundle.name) },
Text(
text = bundle.name,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
},
supportingContent = { supportingContent = {
state.patchBundleOrNull()?.patches?.size?.let { patchCount -> state.patchBundleOrNull()?.patches?.size?.let { patchCount ->
Text( Text(text = pluralStringResource(R.plurals.patch_count, patchCount, patchCount))
text = pluralStringResource(R.plurals.patch_count, patchCount, patchCount),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} }
}, },
trailingContent = { trailingContent = {
@ -114,13 +102,7 @@ fun BundleItem(
) )
} }
version?.let { txt -> version?.let { Text(text = it) }
Text(
text = txt,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} }
}, },
) )

View file

@ -27,7 +27,7 @@ sealed interface SettingsDestination : Parcelable {
object About : SettingsDestination object About : SettingsDestination
@Parcelize @Parcelize
data class Update(val downloadOnScreenEntry: Boolean) : SettingsDestination data class Update(val downloadOnScreenEntry: Boolean = false) : SettingsDestination
@Parcelize @Parcelize
object Changelogs : SettingsDestination object Changelogs : SettingsDestination

View file

@ -2,6 +2,7 @@ package app.revanced.manager.ui.screen
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -15,6 +16,8 @@ import androidx.compose.material.icons.outlined.DeleteOutline
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Source import androidx.compose.material.icons.outlined.Source
import androidx.compose.material.icons.outlined.Update
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -24,6 +27,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow import androidx.compose.material3.TabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -43,7 +47,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault
import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.component.NotificationCard
import app.revanced.manager.ui.component.bundle.BundleItem import app.revanced.manager.ui.component.bundle.BundleItem
import app.revanced.manager.ui.component.bundle.BundleTopBar import app.revanced.manager.ui.component.bundle.BundleTopBar
import app.revanced.manager.ui.component.bundle.ImportBundleDialog import app.revanced.manager.ui.component.bundle.ImportBundleDialog
@ -68,38 +75,26 @@ fun DashboardScreen(
vm: DashboardViewModel = koinViewModel(), vm: DashboardViewModel = koinViewModel(),
onAppSelectorClick: () -> Unit, onAppSelectorClick: () -> Unit,
onSettingsClick: () -> Unit, onSettingsClick: () -> Unit,
onUpdateClick: () -> Unit,
onAppClick: (InstalledApp) -> Unit onAppClick: (InstalledApp) -> Unit
) { ) {
var showBundleTypeSelectorDialog by rememberSaveable { mutableStateOf(false) }
var selectedBundleType: BundleType? by rememberSaveable { mutableStateOf(null) }
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } } val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
val pages: Array<DashboardPage> = DashboardPage.values()
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0) val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
val androidContext = LocalContext.current val androidContext = LocalContext.current
val composableScope = rememberCoroutineScope()
val pagerState = rememberPagerState( val pagerState = rememberPagerState(
initialPage = DashboardPage.DASHBOARD.ordinal, initialPage = DashboardPage.DASHBOARD.ordinal,
initialPageOffsetFraction = 0f initialPageOffsetFraction = 0f
) { ) { DashboardPage.entries.size }
DashboardPage.values().size
}
val composableScope = rememberCoroutineScope()
LaunchedEffect(pagerState.currentPage) { LaunchedEffect(pagerState.currentPage) {
if (pagerState.currentPage != DashboardPage.BUNDLES.ordinal) vm.cancelSourceSelection() if (pagerState.currentPage != DashboardPage.BUNDLES.ordinal) vm.cancelSourceSelection()
} }
if (showBundleTypeSelectorDialog) { val firstLaunch by vm.prefs.firstLaunch.getAsState()
ImportBundleTypeSelectorDialog( if (firstLaunch) AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
onDismiss = { showBundleTypeSelectorDialog = false },
onConfirm = {
selectedBundleType = it
showBundleTypeSelectorDialog = false
}
)
}
var selectedBundleType: BundleType? by rememberSaveable { mutableStateOf(null) }
selectedBundleType?.let { selectedBundleType?.let {
fun dismiss() { fun dismiss() {
selectedBundleType = null selectedBundleType = null
@ -119,6 +114,17 @@ fun DashboardScreen(
) )
} }
var showBundleTypeSelectorDialog by rememberSaveable { mutableStateOf(false) }
if (showBundleTypeSelectorDialog) {
ImportBundleTypeSelectorDialog(
onDismiss = { showBundleTypeSelectorDialog = false },
onConfirm = {
selectedBundleType = it
showBundleTypeSelectorDialog = false
}
)
}
Scaffold( Scaffold(
topBar = { topBar = {
if (bundlesSelectable) { if (bundlesSelectable) {
@ -192,9 +198,7 @@ fun DashboardScreen(
} }
} }
} }
) { ) { Icon(Icons.Default.Add, stringResource(R.string.add)) }
Icon(Icons.Default.Add, stringResource(R.string.add))
}
} }
) { paddingValues -> ) { paddingValues ->
Column(Modifier.padding(paddingValues)) { Column(Modifier.padding(paddingValues)) {
@ -202,7 +206,7 @@ fun DashboardScreen(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) { ) {
pages.forEachIndexed { index, page -> DashboardPage.entries.forEachIndexed { index, page ->
Tab( Tab(
selected = pagerState.currentPage == index, selected = pagerState.currentPage == index,
onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } }, onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } },
@ -214,12 +218,41 @@ fun DashboardScreen(
} }
} }
Notifications(
if (!Aapt.supportsDevice()) {
{
NotificationCard(
isWarning = true,
icon = Icons.Outlined.WarningAmber,
text = stringResource(R.string.unsupported_architecture_warning),
onDismiss = null
)
}
} else null,
vm.updatedManagerVersion?.let {
{
NotificationCard(
text = stringResource(R.string.update_available_dialog_description, it),
icon = Icons.Outlined.Update,
actions = {
TextButton(onClick = vm::dismissUpdateDialog) {
Text(stringResource(R.string.dismiss))
}
TextButton(onClick = onUpdateClick) {
Text(stringResource(R.string.update))
}
}
)
}
}
)
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
userScrollEnabled = true, userScrollEnabled = true,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
pageContent = { index -> pageContent = { index ->
when (pages[index]) { when (DashboardPage.entries[index]) {
DashboardPage.DASHBOARD -> { DashboardPage.DASHBOARD -> {
InstalledAppsScreen( InstalledAppsScreen(
onAppClick = onAppClick onAppClick = onAppClick
@ -238,11 +271,9 @@ fun DashboardScreen(
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList()) val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
Column( Column(
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize(),
) { ) {
sources.forEach { sources.forEach {
BundleItem( BundleItem(
bundle = it, bundle = it,
onDelete = { onDelete = {
@ -273,3 +304,21 @@ fun DashboardScreen(
} }
} }
} }
@Composable
fun Notifications(
vararg notifications: (@Composable () -> Unit)?,
) {
val activeNotifications = notifications.filterNotNull()
if (activeNotifications.isNotEmpty()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
activeNotifications.forEach { notification ->
notification()
}
}
}
}

View file

@ -6,8 +6,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -20,12 +18,10 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.ui.component.AppIcon import app.revanced.manager.ui.component.AppIcon
import app.revanced.manager.ui.component.AppLabel import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.NotificationCard
import app.revanced.manager.ui.viewmodel.InstalledAppsViewModel import app.revanced.manager.ui.viewmodel.InstalledAppsViewModel
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@ -37,16 +33,6 @@ fun InstalledAppsScreen(
val installedApps by viewModel.apps.collectAsStateWithLifecycle(initialValue = null) val installedApps by viewModel.apps.collectAsStateWithLifecycle(initialValue = null)
Column { Column {
if (!Aapt.supportsDevice()) {
NotificationCard(
isWarning = true,
icon = Icons.Outlined.WarningAmber,
text = stringResource(
R.string.unsupported_architecture_warning
),
)
}
LazyColumnWithScrollbar( LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,

View file

@ -22,6 +22,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ColumnWithScrollbar
@ -161,10 +162,11 @@ fun SettingsScreen(
) { ) {
AnimatedVisibility(visible = showBatteryButton) { AnimatedVisibility(visible = showBatteryButton) {
NotificationCard( NotificationCard(
modifier = Modifier.padding(16.dp),
isWarning = true, isWarning = true,
icon = Icons.Default.BatteryAlert, icon = Icons.Default.BatteryAlert,
text = stringResource(R.string.battery_optimization_notification), text = stringResource(R.string.battery_optimization_notification),
primaryAction = { onClick = {
context.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { context.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${context.packageName}") data = Uri.parse("package:${context.packageName}")
}) })

View file

@ -3,21 +3,32 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.ContentResolver import android.content.ContentResolver
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull
import app.revanced.manager.domain.bundles.RemotePatchBundle import app.revanced.manager.domain.bundles.RemotePatchBundle
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class DashboardViewModel( class DashboardViewModel(
private val app: Application, private val app: Application,
private val patchBundleRepository: PatchBundleRepository private val patchBundleRepository: PatchBundleRepository,
private val reVancedAPI: ReVancedAPI,
private val networkInfo: NetworkInfo,
val prefs: PreferencesManager
) : ViewModel() { ) : ViewModel() {
val availablePatches = val availablePatches =
patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } } patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } }
@ -25,6 +36,47 @@ class DashboardViewModel(
val sources = patchBundleRepository.sources val sources = patchBundleRepository.sources
val selectedSources = mutableStateListOf<PatchBundleSource>() val selectedSources = mutableStateListOf<PatchBundleSource>()
var updatedManagerVersion: String? by mutableStateOf(null)
private set
init {
viewModelScope.launch { checkForManagerUpdates() }
}
fun dismissUpdateDialog() {
updatedManagerVersion = null
}
private suspend fun checkForManagerUpdates() {
if (!prefs.managerAutoUpdates.get() || !networkInfo.isConnected()) return
uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") {
updatedManagerVersion = reVancedAPI.getAppUpdate()?.version
}
}
fun applyAutoUpdatePrefs(manager: Boolean, patches: Boolean) = viewModelScope.launch {
prefs.firstLaunch.update(false)
prefs.managerAutoUpdates.update(manager)
if (manager) checkForManagerUpdates()
if (patches) {
with(patchBundleRepository) {
sources
.first()
.find { it.uid == 0 }
?.asRemoteOrNull
?.setAutoUpdate(true)
updateCheck()
}
}
}
fun cancelSourceSelection() { fun cancelSourceSelection() {
selectedSources.clear() selectedSources.clear()
} }

View file

@ -8,25 +8,18 @@ import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull
import app.revanced.manager.domain.manager.KeystoreManager import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.SerializedSelection import app.revanced.manager.domain.repository.SerializedSelection
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.isDebuggable
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -36,50 +29,9 @@ class MainViewModel(
private val patchBundleRepository: PatchBundleRepository, private val patchBundleRepository: PatchBundleRepository,
private val patchSelectionRepository: PatchSelectionRepository, private val patchSelectionRepository: PatchSelectionRepository,
private val keystoreManager: KeystoreManager, private val keystoreManager: KeystoreManager,
private val reVancedAPI: ReVancedAPI,
private val app: Application, private val app: Application,
private val networkInfo: NetworkInfo,
val prefs: PreferencesManager val prefs: PreferencesManager
) : ViewModel() { ) : ViewModel() {
var updatedManagerVersion: String? by mutableStateOf(null)
private set
init {
viewModelScope.launch { checkForManagerUpdates() }
}
fun dismissUpdateDialog() {
updatedManagerVersion = null
}
private suspend fun checkForManagerUpdates() {
if (app.isDebuggable || !prefs.managerAutoUpdates.get() || !networkInfo.isConnected()) return
uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") {
updatedManagerVersion = reVancedAPI.getAppUpdate()?.version
}
}
fun applyAutoUpdatePrefs(manager: Boolean, patches: Boolean) = viewModelScope.launch {
prefs.firstLaunch.update(false)
prefs.managerAutoUpdates.update(manager)
if (manager) checkForManagerUpdates()
if (patches) {
with(patchBundleRepository) {
sources
.first()
.find { it.uid == 0 }
?.asRemoteOrNull
?.setAutoUpdate(true)
updateCheck()
}
}
}
fun importLegacySettings(componentActivity: ComponentActivity) { fun importLegacySettings(componentActivity: ComponentActivity) {
if (!prefs.firstLaunch.getBlocking()) return if (!prefs.firstLaunch.getBlocking()) return

View file

@ -324,8 +324,7 @@
<string name="no_update_available">No update available</string> <string name="no_update_available">No update available</string>
<string name="update_check">Checking for updates…</string> <string name="update_check">Checking for updates…</string>
<string name="dismiss_temporary">Not now</string> <string name="dismiss_temporary">Not now</string>
<string name="update_available_dialog_title">New update available</string> <string name="update_available_dialog_description">A new version of ReVanced Manager (%s) is available.</string>
<string name="update_available_dialog_description">A new version (%s) is available for download.</string>
<string name="failed_to_download_update">Failed to download update: %s</string> <string name="failed_to_download_update">Failed to download update: %s</string>
<string name="download">Download</string> <string name="download">Download</string>
<string name="download_confirmation_metered">You are currently on a metered connection, and data charges from your service provider may apply.\n\nDo you still want to continue?</string> <string name="download_confirmation_metered">You are currently on a metered connection, and data charges from your service provider may apply.\n\nDo you still want to continue?</string>