feat: switch to androidx.navigation (#2362)

This commit is contained in:
Ax333l 2024-12-23 14:31:31 +01:00 committed by GitHub
parent f9831d4da5
commit 5d3a81f4b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 554 additions and 555 deletions

View File

@ -126,6 +126,7 @@ dependencies {
implementation(libs.compose.livedata)
implementation(libs.compose.material.icons.extended)
implementation(libs.compose.material3)
implementation(libs.navigation.compose)
// Accompanist
implementation(libs.accompanist.drawablepainter)
@ -173,11 +174,9 @@ dependencies {
// Koin
implementation(libs.koin.android)
implementation(libs.koin.compose)
implementation(libs.koin.compose.navigation)
implementation(libs.koin.workmanager)
// Compose Navigation
implementation(libs.reimagined.navigation)
// Licenses
implementation(libs.about.libraries)

View File

@ -1,36 +1,38 @@
package app.revanced.manager
import android.os.Bundle
import android.os.Parcelable
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.destination.SettingsDestination
import app.revanced.manager.ui.screen.AppSelectorScreen
import app.revanced.manager.ui.screen.DashboardScreen
import app.revanced.manager.ui.screen.InstalledAppInfoScreen
import app.revanced.manager.ui.screen.PatcherScreen
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
import app.revanced.manager.ui.screen.SettingsScreen
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import app.revanced.manager.ui.model.navigation.*
import app.revanced.manager.ui.screen.*
import app.revanced.manager.ui.screen.settings.*
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
import app.revanced.manager.ui.theme.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.EventEffect
import dev.olshevski.navigation.reimagined.AnimatedNavHost
import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.navigate
import dev.olshevski.navigation.reimagined.pop
import dev.olshevski.navigation.reimagined.popUpTo
import dev.olshevski.navigation.reimagined.rememberNavController
import org.koin.androidx.compose.koinViewModel
import org.koin.androidx.compose.navigation.koinNavViewModel
import org.koin.core.parameter.parametersOf
import org.koin.androidx.compose.koinViewModel as getComposeViewModel
import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
class MainActivity : ComponentActivity() {
@ExperimentalAnimationApi
@ -41,7 +43,7 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
installSplashScreen()
val vm: MainViewModel = getAndroidViewModel()
val vm: MainViewModel = getActivityViewModel()
vm.importLegacySettings(this)
setContent {
@ -52,79 +54,203 @@ class MainActivity : ComponentActivity() {
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
dynamicColor = dynamicColor
) {
val navController =
rememberNavController<Destination>(startDestination = Destination.Dashboard)
NavBackHandler(navController)
ReVancedManager(vm)
}
}
}
}
@Composable
private fun ReVancedManager(vm: MainViewModel) {
val navController = rememberNavController()
EventEffect(vm.appSelectFlow) { app ->
navController.navigate(Destination.SelectedApplicationInfo(app))
navController.navigateComplex(
SelectedApplicationInfo,
SelectedApplicationInfo.ViewModelParams(app)
)
}
AnimatedNavHost(
controller = navController
) { destination ->
when (destination) {
is Destination.Dashboard -> DashboardScreen(
onSettingsClick = { navController.navigate(Destination.Settings()) },
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
NavHost(
navController = navController,
startDestination = Dashboard,
) {
composable<Dashboard> {
DashboardScreen(
onSettingsClick = { navController.navigate(Settings) },
onAppSelectorClick = {
navController.navigate(AppSelector)
},
onUpdateClick = {
navController.navigate(Destination.Settings(SettingsDestination.Update()))
navController.navigate(Update())
},
onDownloaderPluginClick = {
navController.navigate(Destination.Settings(SettingsDestination.Downloads))
navController.navigate(Settings.Downloads)
},
onAppClick = { installedApp ->
navController.navigate(
Destination.InstalledApplicationInfo(
installedApp
)
)
onAppClick = { packageName ->
navController.navigate(InstalledApplicationInfo(packageName))
}
)
}
is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen(
composable<InstalledApplicationInfo> {
val data = it.toRoute<InstalledApplicationInfo>()
InstalledAppInfoScreen(
onPatchClick = vm::selectApp,
onBackClick = { navController.pop() },
viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
onBackClick = navController::popBackStack,
viewModel = koinViewModel { parametersOf(data.packageName) }
)
}
is Destination.Settings -> SettingsScreen(
onBackClick = { navController.pop() },
startDestination = destination.startDestination
)
is Destination.AppSelector -> AppSelectorScreen(
composable<AppSelector> {
AppSelectorScreen(
onSelect = vm::selectApp,
onStorageSelect = vm::selectApp,
onBackClick = { navController.pop() }
onBackClick = navController::popBackStack
)
}
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
composable<Patcher> {
PatcherScreen(
onBackClick = {
navController.navigate(route = Dashboard) {
launchSingleTop = true
popUpTo<Dashboard> {
inclusive = false
}
}
},
vm = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
)
}
composable<Update> {
val data = it.toRoute<Update>()
UpdateScreen(
onBackClick = navController::popBackStack,
vm = koinViewModel { parametersOf(data.downloadOnScreenEntry) }
)
}
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
composable<SelectedApplicationInfo.Main> {
val parentBackStackEntry = navController.navGraphEntry(it)
val data =
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
SelectedAppInfoScreen(
onBackClick = navController::popBackStack,
onPatchClick = { app, patches, options ->
navController.navigate(
Destination.Patcher(
app, patches, options
navController.navigateComplex(
Patcher,
Patcher.ViewModelParams(app, patches, options)
)
},
onPatchSelectorClick = { app, patches, options ->
navController.navigateComplex(
SelectedApplicationInfo.PatchesSelector,
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
app,
patches,
options
)
)
},
onBackClick = navController::pop,
vm = getComposeViewModel {
parametersOf(
SelectedAppInfoViewModel.Params(
destination.selectedApp,
destination.patchSelection
)
vm = koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
parametersOf(data)
}
)
}
composable<SelectedApplicationInfo.PatchesSelector> {
val data =
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
viewModelStoreOwner = navController.navGraphEntry(it)
)
is Destination.Patcher -> PatcherScreen(
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
vm = getComposeViewModel { parametersOf(destination) }
PatchesSelectorScreen(
onBackClick = navController::popBackStack,
onSave = { patches, options ->
selectedAppInfoVm.updateConfiguration(patches, options)
navController.popBackStack()
},
vm = koinViewModel { parametersOf(data) }
)
}
}
navigation<Settings>(startDestination = Settings.Main) {
composable<Settings.Main> {
SettingsScreen(
onBackClick = navController::popBackStack,
navigate = navController::navigate
)
}
composable<Settings.General> {
GeneralSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Advanced> {
AdvancedSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Updates> {
UpdatesSettingsScreen(
onBackClick = navController::popBackStack,
onChangelogClick = { navController.navigate(Settings.Changelogs) },
onUpdateClick = { navController.navigate(Update()) }
)
}
composable<Settings.Downloads> {
DownloadsSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.ImportExport> {
ImportExportSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.About> {
AboutSettingsScreen(
onBackClick = navController::popBackStack,
navigate = navController::navigate
)
}
composable<Settings.Changelogs> {
ChangelogsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Contributors> {
ContributorScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Licenses> {
LicensesScreen(onBackClick = navController::popBackStack)
}
composable<Settings.DeveloperOptions> {
DeveloperOptionsScreen(onBackClick = navController::popBackStack)
}
}
}
}
@Composable
private fun NavController.navGraphEntry(entry: NavBackStackEntry) =
remember(entry) { getBackStackEntry(entry.destination.parent!!.id) }
// Androidx Navigation does not support storing complex types in route objects, so we have to store them inside the saved state handle of the back stack entry instead.
private fun <T : Parcelable, R : ComplexParameter<T>> NavController.navigateComplex(
route: R,
data: T
) {
navigate(route)
getBackStackEntry(route).savedStateHandle["args"] = data
}
private fun <T : Parcelable> NavBackStackEntry.getComplexArg() = savedStateHandle.get<T>("args")!!

View File

@ -1,18 +1,15 @@
package app.revanced.manager.data.room.apps.installed
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import app.revanced.manager.R
import kotlinx.parcelize.Parcelize
enum class InstallType(val stringResource: Int) {
DEFAULT(R.string.default_install),
MOUNT(R.string.mount_install)
}
@Parcelize
@Entity(tableName = "installed_app")
data class InstalledApp(
@PrimaryKey
@ -20,4 +17,4 @@ data class InstalledApp(
@ColumnInfo(name = "original_package_name") val originalPackageName: String,
@ColumnInfo(name = "version") val version: String,
@ColumnInfo(name = "install_type") val installType: InstallType
) : Parcelable
)

View File

@ -9,7 +9,7 @@ val viewModelModule = module {
viewModelOf(::DashboardViewModel)
viewModelOf(::SelectedAppInfoViewModel)
viewModelOf(::PatchesSelectorViewModel)
viewModelOf(::SettingsViewModel)
viewModelOf(::GeneralSettingsViewModel)
viewModelOf(::AdvancedSettingsViewModel)
viewModelOf(::AppSelectorViewModel)
viewModelOf(::PatcherViewModel)

View File

@ -1,31 +0,0 @@
package app.revanced.manager.ui.destination
import android.os.Parcelable
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
sealed interface Destination : Parcelable {
@Parcelize
data object Dashboard : Destination
@Parcelize
data class InstalledApplicationInfo(val installedApp: InstalledApp) : Destination
@Parcelize
data object AppSelector : Destination
@Parcelize
data class Settings(val startDestination: SettingsDestination = SettingsDestination.Settings) : Destination
@Parcelize
data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchSelection: PatchSelection? = null) : Destination
@Parcelize
data class Patcher(val selectedApp: SelectedApp, val selectedPatches: PatchSelection, val options: @RawValue Options) : Destination
}

View File

@ -1,16 +0,0 @@
package app.revanced.manager.ui.destination
import android.os.Parcelable
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
sealed interface SelectedAppInfoDestination : Parcelable {
@Parcelize
data object Main : SelectedAppInfoDestination
@Parcelize
data class PatchesSelector(val app: SelectedApp, val currentSelection: PatchSelection?, val options: @RawValue Options) : SelectedAppInfoDestination
}

View File

@ -1,43 +0,0 @@
package app.revanced.manager.ui.destination
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
sealed interface SettingsDestination : Parcelable {
@Parcelize
data object Settings : SettingsDestination
@Parcelize
data object General : SettingsDestination
@Parcelize
data object Advanced : SettingsDestination
@Parcelize
data object Updates : SettingsDestination
@Parcelize
data object Downloads : SettingsDestination
@Parcelize
data object ImportExport : SettingsDestination
@Parcelize
data object About : SettingsDestination
@Parcelize
data class Update(val downloadOnScreenEntry: Boolean = false) : SettingsDestination
@Parcelize
data object Changelogs : SettingsDestination
@Parcelize
data object Contributors: SettingsDestination
@Parcelize
data object Licenses: SettingsDestination
@Parcelize
data object DeveloperOptions: SettingsDestination
}

View File

@ -0,0 +1,93 @@
package app.revanced.manager.ui.model.navigation
import android.os.Parcelable
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import kotlinx.serialization.Serializable
interface ComplexParameter<T : Parcelable>
@Serializable
object Dashboard
@Serializable
object AppSelector
@Serializable
data class InstalledApplicationInfo(val packageName: String)
@Serializable
data class Update(val downloadOnScreenEntry: Boolean = false)
@Serializable
data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.ViewModelParams> {
@Parcelize
data class ViewModelParams(
val app: SelectedApp,
val patches: PatchSelection? = null
) : Parcelable
@Serializable
object Main
@Serializable
data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> {
@Parcelize
data class ViewModelParams(
val app: SelectedApp,
val currentSelection: PatchSelection?,
val options: @RawValue Options,
) : Parcelable
}
}
@Serializable
data object Patcher : ComplexParameter<Patcher.ViewModelParams> {
@Parcelize
data class ViewModelParams(
val selectedApp: SelectedApp,
val selectedPatches: PatchSelection,
val options: @RawValue Options
) : Parcelable
}
@Serializable
object Settings {
sealed interface Destination
@Serializable
data object Main : Destination
@Serializable
data object General : Destination
@Serializable
data object Advanced : Destination
@Serializable
data object Updates : Destination
@Serializable
data object Downloads : Destination
@Serializable
data object ImportExport : Destination
@Serializable
data object About : Destination
@Serializable
data object Changelogs : Destination
@Serializable
data object Contributors : Destination
@Serializable
data object Licenses : Destination
@Serializable
data object DeveloperOptions : Destination
}

View File

@ -25,7 +25,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.ui.component.AlertDialogExtended
@ -60,7 +59,7 @@ fun DashboardScreen(
onSettingsClick: () -> Unit,
onUpdateClick: () -> Unit,
onDownloaderPluginClick: () -> Unit,
onAppClick: (InstalledApp) -> Unit
onAppClick: (String) -> Unit
) {
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
@ -289,7 +288,7 @@ fun DashboardScreen(
when (DashboardPage.entries[index]) {
DashboardPage.DASHBOARD -> {
InstalledAppsScreen(
onAppClick = onAppClick
onAppClick = { onAppClick(it.currentPackageName) }
)
}

View File

@ -77,10 +77,12 @@ fun InstalledAppInfoScreen(
.fillMaxSize()
.padding(paddingValues)
) {
AppInfo(viewModel.appInfo) {
Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
val installedApp = viewModel.installedApp ?: return@ColumnWithScrollbar
if (viewModel.installedApp.installType == InstallType.MOUNT) {
AppInfo(viewModel.appInfo) {
Text(installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
if (installedApp.installType == InstallType.MOUNT) {
Text(
text = if (viewModel.isMounted) {
stringResource(R.string.mounted)
@ -104,7 +106,7 @@ fun InstalledAppInfoScreen(
onClick = viewModel::launch
)
when (viewModel.installedApp.installType) {
when (installedApp.installType) {
InstallType.DEFAULT -> SegmentedButton(
icon = Icons.Outlined.Delete,
text = stringResource(R.string.uninstall),
@ -133,9 +135,9 @@ fun InstalledAppInfoScreen(
icon = Icons.Outlined.Update,
text = stringResource(R.string.repatch),
onClick = {
onPatchClick(viewModel.installedApp.originalPackageName)
onPatchClick(installedApp.originalPackageName)
},
enabled = viewModel.installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess()
enabled = installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess()
)
}
@ -158,19 +160,19 @@ fun InstalledAppInfoScreen(
SettingsListItem(
headlineContent = stringResource(R.string.package_name),
supportingContent = viewModel.installedApp.currentPackageName
supportingContent = installedApp.currentPackageName
)
if (viewModel.installedApp.originalPackageName != viewModel.installedApp.currentPackageName) {
if (installedApp.originalPackageName != installedApp.currentPackageName) {
SettingsListItem(
headlineContent = stringResource(R.string.original_package_name),
supportingContent = viewModel.installedApp.originalPackageName
supportingContent = installedApp.originalPackageName
)
}
SettingsListItem(
headlineContent = stringResource(R.string.install_type),
supportingContent = stringResource(viewModel.installedApp.installType.stringResource)
supportingContent = stringResource(installedApp.installType.stringResource)
)
}
}

View File

@ -32,10 +32,8 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.destination.SelectedAppInfoDestination
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.EventEffect
import app.revanced.manager.util.Options
@ -43,13 +41,11 @@ import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.enabled
import app.revanced.manager.util.toast
import app.revanced.manager.util.transparentListItemColors
import dev.olshevski.navigation.reimagined.*
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SelectedAppInfoScreen(
onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit,
onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit,
onBackClick: () -> Unit,
vm: SelectedAppInfoViewModel
@ -82,15 +78,8 @@ fun SelectedAppInfoScreen(
launcher.launch(intent)
}
val navController =
rememberNavController<SelectedAppInfoDestination>(startDestination = SelectedAppInfoDestination.Main)
NavBackHandler(controller = navController)
AnimatedNavHost(controller = navController) { destination ->
val error by vm.errorFlow.collectAsStateWithLifecycle(null)
when (destination) {
is SelectedAppInfoDestination.Main -> Scaffold(
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.app_info),
@ -167,8 +156,7 @@ fun SelectedAppInfoScreen(
selectedPatchCount
),
onClick = {
navController.navigate(
SelectedAppInfoDestination.PatchesSelector(
onPatchSelectorClick(
vm.selectedApp,
vm.getCustomPatches(
bundles,
@ -176,7 +164,6 @@ fun SelectedAppInfoScreen(
),
vm.options
)
)
}
)
PageItem(
@ -205,25 +192,6 @@ fun SelectedAppInfoScreen(
}
}
}
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
onSave = { patches, options ->
vm.updateConfiguration(patches, options, bundles)
navController.pop()
},
onBackClick = navController::pop,
vm = koinViewModel {
parametersOf(
PatchesSelectorViewModel.Params(
destination.app,
destination.currentSelection,
destination.options,
)
)
}
)
}
}
}
@Composable

View File

@ -13,128 +13,49 @@ import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.destination.SettingsDestination
import app.revanced.manager.ui.screen.settings.*
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
import app.revanced.manager.ui.screen.settings.update.UpdateScreen
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
import app.revanced.manager.ui.viewmodel.SettingsViewModel
import dev.olshevski.navigation.reimagined.*
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
import app.revanced.manager.ui.model.navigation.Settings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onBackClick: () -> Unit,
startDestination: SettingsDestination,
viewModel: SettingsViewModel = koinViewModel()
) {
val navController = rememberNavController(startDestination)
val backClick: () -> Unit = {
if (navController.backstack.entries.size == 1)
onBackClick()
else navController.pop()
}
val settingsSections = listOf(
private val settingsSections = listOf(
Triple(
R.string.general,
R.string.general_description,
Icons.Outlined.Settings
) to SettingsDestination.General,
) to Settings.General,
Triple(
R.string.updates,
R.string.updates_description,
Icons.Outlined.Update
) to SettingsDestination.Updates,
) to Settings.Updates,
Triple(
R.string.downloads,
R.string.downloads_description,
Icons.Outlined.Download
) to SettingsDestination.Downloads,
) to Settings.Downloads,
Triple(
R.string.import_export,
R.string.import_export_description,
Icons.Outlined.SwapVert
) to SettingsDestination.ImportExport,
) to Settings.ImportExport,
Triple(
R.string.advanced,
R.string.advanced_description,
Icons.Outlined.Tune
) to SettingsDestination.Advanced,
) to Settings.Advanced,
Triple(
R.string.about,
R.string.app_name,
Icons.Outlined.Info
) to SettingsDestination.About,
)
NavBackHandler(navController)
AnimatedNavHost(
controller = navController
) { destination ->
when (destination) {
is SettingsDestination.General -> GeneralSettingsScreen(
onBackClick = backClick,
viewModel = viewModel
) to Settings.About,
)
is SettingsDestination.Advanced -> AdvancedSettingsScreen(
onBackClick = backClick
)
is SettingsDestination.Updates -> UpdatesSettingsScreen(
onBackClick = backClick,
onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) },
onUpdateClick = { navController.navigate(SettingsDestination.Update(false)) }
)
is SettingsDestination.Downloads -> DownloadsSettingsScreen(
onBackClick = backClick
)
is SettingsDestination.ImportExport -> ImportExportSettingsScreen(
onBackClick = backClick
)
is SettingsDestination.About -> AboutSettingsScreen(
onBackClick = backClick,
onContributorsClick = { navController.navigate(SettingsDestination.Contributors) },
onDeveloperOptionsClick = { navController.navigate(SettingsDestination.DeveloperOptions) },
onLicensesClick = { navController.navigate(SettingsDestination.Licenses) },
)
is SettingsDestination.Update -> UpdateScreen(
onBackClick = backClick,
vm = koinViewModel {
parametersOf(
destination.downloadOnScreenEntry
)
}
)
is SettingsDestination.Changelogs -> ChangelogsScreen(
onBackClick = backClick,
)
is SettingsDestination.Contributors -> ContributorScreen(
onBackClick = backClick,
)
is SettingsDestination.Licenses -> LicensesScreen(
onBackClick = backClick,
)
is SettingsDestination.DeveloperOptions -> DeveloperOptionsScreen(onBackClick = backClick)
is SettingsDestination.Settings -> {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> Unit) {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.settings),
onBackClick = backClick,
onBackClick = onBackClick,
)
}
) { paddingValues ->
@ -145,7 +66,7 @@ fun SettingsScreen(
) {
settingsSections.forEach { (titleDescIcon, destination) ->
SettingsListItem(
modifier = Modifier.clickable { navController.navigate(destination) },
modifier = Modifier.clickable { navigate(destination) },
headlineContent = stringResource(titleDescIcon.first),
supportingContent = stringResource(titleDescIcon.second),
leadingContent = { Icon(titleDescIcon.third, null) }
@ -154,6 +75,3 @@ fun SettingsScreen(
}
}
}
}
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.manager.ui.screen.settings.update
package app.revanced.manager.ui.screen
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.spring

View File

@ -37,6 +37,7 @@ import app.revanced.manager.network.dto.ReVancedSocial
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.model.navigation.Settings
import app.revanced.manager.ui.viewmodel.AboutViewModel
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon
import app.revanced.manager.util.openUrl
@ -47,9 +48,7 @@ import org.koin.androidx.compose.koinViewModel
@Composable
fun AboutSettingsScreen(
onBackClick: () -> Unit,
onContributorsClick: () -> Unit,
onLicensesClick: () -> Unit,
onDeveloperOptionsClick: () -> Unit,
navigate: (Settings.Destination) -> Unit,
viewModel: AboutViewModel = koinViewModel()
) {
val context = LocalContext.current
@ -114,17 +113,17 @@ fun AboutSettingsScreen(
Triple(
stringResource(R.string.contributors),
stringResource(R.string.contributors_description),
third = onContributorsClick
third = { navigate(Settings.Contributors) }
),
Triple(
stringResource(R.string.developer_options),
stringResource(R.string.developer_options_description),
third = onDeveloperOptionsClick
third = { navigate(Settings.DeveloperOptions) }
),
Triple(
stringResource(R.string.opensource_licenses),
stringResource(R.string.opensource_licenses_description),
third = onLicensesClick
third = { navigate(Settings.Licenses) }
)
)

View File

@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@ -32,14 +31,15 @@ import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.SettingsViewModel
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GeneralSettingsScreen(
onBackClick: () -> Unit,
viewModel: SettingsViewModel
viewModel: GeneralSettingsViewModel = koinViewModel()
) {
val prefs = viewModel.prefs
val coroutineScope = viewModel.viewModelScope

View File

@ -6,7 +6,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.theme.Theme
import kotlinx.coroutines.launch
class SettingsViewModel(
class GeneralSettingsViewModel(
val prefs: PreferencesManager
) : ViewModel() {
fun setTheme(theme: Theme) = viewModelScope.launch {

View File

@ -32,15 +32,17 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class InstalledAppInfoViewModel(
val installedApp: InstalledApp
packageName: String
) : ViewModel(), KoinComponent {
private val app: Application by inject()
private val context: Application by inject()
private val pm: PM by inject()
private val installedAppRepository: InstalledAppRepository by inject()
val rootInstaller: RootInstaller by inject()
lateinit var onBackClick: () -> Unit
var installedApp: InstalledApp? by mutableStateOf(null)
private set
var appInfo: PackageInfo? by mutableStateOf(null)
private set
var appliedPatches: PatchSelection? by mutableStateOf(null)
@ -49,38 +51,48 @@ class InstalledAppInfoViewModel(
init {
viewModelScope.launch {
isMounted = rootInstaller.isAppMounted(installedApp.currentPackageName)
installedApp = installedAppRepository.get(packageName)?.also {
isMounted = rootInstaller.isAppMounted(it.currentPackageName)
appInfo = withContext(Dispatchers.IO) {
pm.getPackageInfo(it.currentPackageName)
}
appliedPatches = withContext(Dispatchers.IO) {
installedAppRepository.getAppliedPatches(it.currentPackageName)
}
}
}
}
fun launch() = pm.launch(installedApp.currentPackageName)
fun launch() = installedApp?.currentPackageName?.let(pm::launch)
fun mountOrUnmount() = viewModelScope.launch {
val pkgName = installedApp?.currentPackageName ?: return@launch
try {
if (isMounted)
rootInstaller.unmount(installedApp.currentPackageName)
rootInstaller.unmount(pkgName)
else
rootInstaller.mount(installedApp.currentPackageName)
rootInstaller.mount(pkgName)
} catch (e: Exception) {
if (isMounted) {
app.toast(app.getString(R.string.failed_to_unmount, e.simpleMessage()))
context.toast(context.getString(R.string.failed_to_unmount, e.simpleMessage()))
Log.e(tag, "Failed to unmount", e)
} else {
app.toast(app.getString(R.string.failed_to_mount, e.simpleMessage()))
context.toast(context.getString(R.string.failed_to_mount, e.simpleMessage()))
Log.e(tag, "Failed to mount", e)
}
} finally {
isMounted = rootInstaller.isAppMounted(installedApp.currentPackageName)
isMounted = rootInstaller.isAppMounted(pkgName)
}
}
fun uninstall() {
when (installedApp.installType) {
InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName)
val app = installedApp ?: return
when (app.installType) {
InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName)
InstallType.MOUNT -> viewModelScope.launch {
rootInstaller.uninstall(installedApp.currentPackageName)
installedAppRepository.delete(installedApp)
rootInstaller.uninstall(app.currentPackageName)
installedAppRepository.delete(app)
onBackClick()
}
}
@ -97,34 +109,22 @@ class InstalledAppInfoViewModel(
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
viewModelScope.launch {
installedAppRepository.delete(installedApp)
installedApp?.let {
installedAppRepository.delete(it)
}
onBackClick()
}
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
app.toast(app.getString(R.string.uninstall_app_fail, extraStatusMessage))
this@InstalledAppInfoViewModel.context.toast(this@InstalledAppInfoViewModel.context.getString(R.string.uninstall_app_fail, extraStatusMessage))
}
}
}
}
}
init {
viewModelScope.launch {
appInfo = withContext(Dispatchers.IO) {
pm.getPackageInfo(installedApp.currentPackageName)
}
}
viewModelScope.launch {
appliedPatches = withContext(Dispatchers.IO) {
installedAppRepository.getAppliedPatches(installedApp.currentPackageName)
}
}
}.also {
ContextCompat.registerReceiver(
app,
uninstallBroadcastReceiver,
context,
it,
IntentFilter(UninstallService.APP_UNINSTALL_ACTION),
ContextCompat.RECEIVER_NOT_EXPORTED
)
@ -132,6 +132,6 @@ class InstalledAppInfoViewModel(
override fun onCleared() {
super.onCleared()
app.unregisterReceiver(uninstallBroadcastReceiver)
context.unregisterReceiver(uninstallBroadcastReceiver)
}
}

View File

@ -39,7 +39,6 @@ import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService
import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.model.InstallerModel
import app.revanced.manager.ui.model.ProgressKey
import app.revanced.manager.ui.model.SelectedApp
@ -47,6 +46,7 @@ import app.revanced.manager.ui.model.State
import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.StepProgressProvider
import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.util.PM
import app.revanced.manager.util.saveableVar
import app.revanced.manager.util.saver.snapshotStateListSaver
@ -72,7 +72,7 @@ import java.time.Duration
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
class PatcherViewModel(
private val input: Destination.Patcher
private val input: Patcher.ViewModelParams
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
private val app: Application by inject()
private val fs: Filesystem by inject()

View File

@ -23,6 +23,7 @@ import app.revanced.manager.ui.model.BundleInfo
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.saver.Nullable
@ -40,7 +41,7 @@ import kotlinx.coroutines.flow.map
@Stable
@OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) : ViewModel(), KoinComponent {
private val app: Application = get()
private val savedStateHandle: SavedStateHandle = get()
private val prefs: PreferencesManager = get()
@ -214,12 +215,6 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
private val selectionSaver: Saver<PersistentPatchSelection?, Nullable<PatchSelection>> =
nullableSaver(persistentMapSaver(valueSaver = persistentSetSaver()))
}
data class Params(
val app: SelectedApp,
val currentSelection: PatchSelection?,
val options: Options,
)
}
// Versions of other types, but utilizing persistent/observable collection types.

View File

@ -33,8 +33,10 @@ import app.revanced.manager.plugin.downloader.GetScope
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.ui.model.BundleInfo
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
import app.revanced.manager.util.Options
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection
@ -57,7 +59,9 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.get
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
class SelectedAppInfoViewModel(
input: SelectedApplicationInfo.ViewModelParams
) : ViewModel(), KoinComponent {
private val app: Application = get()
val bundlesRepo: PatchBundleRepository = get()
private val bundleRepository: PatchBundleRepository = get()
@ -110,7 +114,10 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
}
}
val requiredVersion = combine(prefs.suggestedVersionSafeguard.flow, bundleRepository.suggestedVersions) { suggestedVersionSafeguard, suggestedVersions ->
val requiredVersion = combine(
prefs.suggestedVersionSafeguard.flow,
bundleRepository.suggestedVersions
) { suggestedVersionSafeguard, suggestedVersions ->
if (!suggestedVersionSafeguard) return@combine null
suggestedVersions[input.app.packageName]
@ -264,17 +271,15 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
): PatchSelection? =
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
fun updateConfiguration(
selection: PatchSelection?,
options: Options,
bundles: List<BundleInfo>
) {
fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch {
val bundles = bundlesRepo.bundleInfoFlow(packageName, selectedApp.version).first()
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
val filteredOptions = options.filtered(bundles)
this.options = filteredOptions
this@SelectedAppInfoViewModel.options = filteredOptions
if (!persistConfiguration) return
if (!persistConfiguration) return@launch
viewModelScope.launch(Dispatchers.Default) {
selection?.let { selectionRepository.updateSelection(packageName, it) }
?: selectionRepository.clearSelection(packageName)
@ -283,11 +288,6 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
}
}
data class Params(
val app: SelectedApp,
val patches: PatchSelection?,
)
enum class Error(@StringRes val resourceId: Int) {
NoPlugins(R.string.downloader_no_plugins_available)
}

View File

@ -3,11 +3,6 @@ package app.revanced.manager.util
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.icu.number.Notation
import android.icu.number.NumberFormatter
import android.icu.number.Precision
import android.icu.text.CompactDecimalFormat
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.annotation.MainThread

View File

@ -9,6 +9,7 @@ appcompat = "1.7.0"
preferences-datastore = "1.1.1"
work-runtime = "2.10.0"
compose-bom = "2024.12.01"
navigation = "2.8.5"
accompanist = "0.34.0"
placeholder = "1.1.2"
reorderable = "1.5.2"
@ -18,9 +19,7 @@ datetime = "0.6.0"
room-version = "2.6.1"
revanced-patcher = "21.0.0"
revanced-library = "3.0.2"
koin-version = "3.5.3"
koin-version-compose = "3.5.3"
reimagined-navigation = "1.5.0"
koin = "3.5.3"
ktor = "2.3.9"
markdown-renderer = "0.22.0"
fading-edges = "1.0.4"
@ -59,6 +58,7 @@ compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", versi
compose-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
# Coil
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
@ -85,12 +85,10 @@ revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.
revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" }
# Koin
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin-version" }
koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin-version-compose" }
koin-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin-version" }
# Compose Navigation
reimagined-navigation = { group = "dev.olshevski.navigation", name = "reimagined", version.ref = "reimagined-navigation" }
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }
koin-compose-navigation = { group = "io.insert-koin", name = "koin-androidx-compose-navigation", version.ref = "koin" }
koin-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" }
# About Libraries
about-libraries = { group = "com.mikepenz", name = "aboutlibraries-compose", version.ref = "about-libraries-gradle-plugin" }