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

View File

@ -1,36 +1,38 @@
package app.revanced.manager package app.revanced.manager
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import app.revanced.manager.ui.destination.Destination import androidx.navigation.NavBackStackEntry
import app.revanced.manager.ui.destination.SettingsDestination import androidx.navigation.NavController
import app.revanced.manager.ui.screen.AppSelectorScreen import androidx.navigation.compose.NavHost
import app.revanced.manager.ui.screen.DashboardScreen import androidx.navigation.compose.composable
import app.revanced.manager.ui.screen.InstalledAppInfoScreen import androidx.navigation.compose.navigation
import app.revanced.manager.ui.screen.PatcherScreen import androidx.navigation.compose.rememberNavController
import app.revanced.manager.ui.screen.SelectedAppInfoScreen import androidx.navigation.toRoute
import app.revanced.manager.ui.screen.SettingsScreen 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.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel import app.revanced.manager.ui.viewmodel.MainViewModel
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.EventEffect import app.revanced.manager.util.EventEffect
import dev.olshevski.navigation.reimagined.AnimatedNavHost import org.koin.androidx.compose.koinViewModel
import dev.olshevski.navigation.reimagined.NavBackHandler import org.koin.androidx.compose.navigation.koinNavViewModel
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.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import org.koin.androidx.compose.koinViewModel as getComposeViewModel import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ExperimentalAnimationApi @ExperimentalAnimationApi
@ -41,7 +43,7 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
installSplashScreen() installSplashScreen()
val vm: MainViewModel = getAndroidViewModel() val vm: MainViewModel = getActivityViewModel()
vm.importLegacySettings(this) vm.importLegacySettings(this)
setContent { setContent {
@ -52,79 +54,203 @@ class MainActivity : ComponentActivity() {
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK, darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
dynamicColor = dynamicColor dynamicColor = dynamicColor
) { ) {
val navController = ReVancedManager(vm)
rememberNavController<Destination>(startDestination = Destination.Dashboard)
NavBackHandler(navController)
EventEffect(vm.appSelectFlow) { app ->
navController.navigate(Destination.SelectedApplicationInfo(app))
}
AnimatedNavHost(
controller = navController
) { destination ->
when (destination) {
is Destination.Dashboard -> DashboardScreen(
onSettingsClick = { navController.navigate(Destination.Settings()) },
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
onUpdateClick = {
navController.navigate(Destination.Settings(SettingsDestination.Update()))
},
onDownloaderPluginClick = {
navController.navigate(Destination.Settings(SettingsDestination.Downloads))
},
onAppClick = { installedApp ->
navController.navigate(
Destination.InstalledApplicationInfo(
installedApp
)
)
}
)
is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen(
onPatchClick = vm::selectApp,
onBackClick = { navController.pop() },
viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
)
is Destination.Settings -> SettingsScreen(
onBackClick = { navController.pop() },
startDestination = destination.startDestination
)
is Destination.AppSelector -> AppSelectorScreen(
onSelect = vm::selectApp,
onStorageSelect = vm::selectApp,
onBackClick = { navController.pop() }
)
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
onPatchClick = { app, patches, options ->
navController.navigate(
Destination.Patcher(
app, patches, options
)
)
},
onBackClick = navController::pop,
vm = getComposeViewModel {
parametersOf(
SelectedAppInfoViewModel.Params(
destination.selectedApp,
destination.patchSelection
)
)
}
)
is Destination.Patcher -> PatcherScreen(
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
vm = getComposeViewModel { parametersOf(destination) }
)
}
}
} }
} }
} }
} }
@Composable
private fun ReVancedManager(vm: MainViewModel) {
val navController = rememberNavController()
EventEffect(vm.appSelectFlow) { app ->
navController.navigateComplex(
SelectedApplicationInfo,
SelectedApplicationInfo.ViewModelParams(app)
)
}
NavHost(
navController = navController,
startDestination = Dashboard,
) {
composable<Dashboard> {
DashboardScreen(
onSettingsClick = { navController.navigate(Settings) },
onAppSelectorClick = {
navController.navigate(AppSelector)
},
onUpdateClick = {
navController.navigate(Update())
},
onDownloaderPluginClick = {
navController.navigate(Settings.Downloads)
},
onAppClick = { packageName ->
navController.navigate(InstalledApplicationInfo(packageName))
}
)
}
composable<InstalledApplicationInfo> {
val data = it.toRoute<InstalledApplicationInfo>()
InstalledAppInfoScreen(
onPatchClick = vm::selectApp,
onBackClick = navController::popBackStack,
viewModel = koinViewModel { parametersOf(data.packageName) }
)
}
composable<AppSelector> {
AppSelectorScreen(
onSelect = vm::selectApp,
onStorageSelect = vm::selectApp,
onBackClick = navController::popBackStack
)
}
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.navigateComplex(
Patcher,
Patcher.ViewModelParams(app, patches, options)
)
},
onPatchSelectorClick = { app, patches, options ->
navController.navigateComplex(
SelectedApplicationInfo.PatchesSelector,
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
app,
patches,
options
)
)
},
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)
)
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 package app.revanced.manager.data.room.apps.installed
import android.os.Parcelable
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import app.revanced.manager.R import app.revanced.manager.R
import kotlinx.parcelize.Parcelize
enum class InstallType(val stringResource: Int) { enum class InstallType(val stringResource: Int) {
DEFAULT(R.string.default_install), DEFAULT(R.string.default_install),
MOUNT(R.string.mount_install) MOUNT(R.string.mount_install)
} }
@Parcelize
@Entity(tableName = "installed_app") @Entity(tableName = "installed_app")
data class InstalledApp( data class InstalledApp(
@PrimaryKey @PrimaryKey
@ -20,4 +17,4 @@ data class InstalledApp(
@ColumnInfo(name = "original_package_name") val originalPackageName: String, @ColumnInfo(name = "original_package_name") val originalPackageName: String,
@ColumnInfo(name = "version") val version: String, @ColumnInfo(name = "version") val version: String,
@ColumnInfo(name = "install_type") val installType: InstallType @ColumnInfo(name = "install_type") val installType: InstallType
) : Parcelable )

View File

@ -9,7 +9,7 @@ val viewModelModule = module {
viewModelOf(::DashboardViewModel) viewModelOf(::DashboardViewModel)
viewModelOf(::SelectedAppInfoViewModel) viewModelOf(::SelectedAppInfoViewModel)
viewModelOf(::PatchesSelectorViewModel) viewModelOf(::PatchesSelectorViewModel)
viewModelOf(::SettingsViewModel) viewModelOf(::GeneralSettingsViewModel)
viewModelOf(::AdvancedSettingsViewModel) viewModelOf(::AdvancedSettingsViewModel)
viewModelOf(::AppSelectorViewModel) viewModelOf(::AppSelectorViewModel)
viewModelOf(::PatcherViewModel) 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.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.domain.bundles.PatchBundleSource.Extensions.isDefault import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
import app.revanced.manager.patcher.aapt.Aapt import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.ui.component.AlertDialogExtended import app.revanced.manager.ui.component.AlertDialogExtended
@ -60,7 +59,7 @@ fun DashboardScreen(
onSettingsClick: () -> Unit, onSettingsClick: () -> Unit,
onUpdateClick: () -> Unit, onUpdateClick: () -> Unit,
onDownloaderPluginClick: () -> Unit, onDownloaderPluginClick: () -> Unit,
onAppClick: (InstalledApp) -> Unit onAppClick: (String) -> Unit
) { ) {
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } } val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0) val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
@ -289,7 +288,7 @@ fun DashboardScreen(
when (DashboardPage.entries[index]) { when (DashboardPage.entries[index]) {
DashboardPage.DASHBOARD -> { DashboardPage.DASHBOARD -> {
InstalledAppsScreen( InstalledAppsScreen(
onAppClick = onAppClick onAppClick = { onAppClick(it.currentPackageName) }
) )
} }

View File

@ -77,10 +77,12 @@ fun InstalledAppInfoScreen(
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) { ) {
AppInfo(viewModel.appInfo) { val installedApp = viewModel.installedApp ?: return@ColumnWithScrollbar
Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
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(
text = if (viewModel.isMounted) { text = if (viewModel.isMounted) {
stringResource(R.string.mounted) stringResource(R.string.mounted)
@ -104,7 +106,7 @@ fun InstalledAppInfoScreen(
onClick = viewModel::launch onClick = viewModel::launch
) )
when (viewModel.installedApp.installType) { when (installedApp.installType) {
InstallType.DEFAULT -> SegmentedButton( InstallType.DEFAULT -> SegmentedButton(
icon = Icons.Outlined.Delete, icon = Icons.Outlined.Delete,
text = stringResource(R.string.uninstall), text = stringResource(R.string.uninstall),
@ -133,9 +135,9 @@ fun InstalledAppInfoScreen(
icon = Icons.Outlined.Update, icon = Icons.Outlined.Update,
text = stringResource(R.string.repatch), text = stringResource(R.string.repatch),
onClick = { 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( SettingsListItem(
headlineContent = stringResource(R.string.package_name), 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( SettingsListItem(
headlineContent = stringResource(R.string.original_package_name), headlineContent = stringResource(R.string.original_package_name),
supportingContent = viewModel.installedApp.originalPackageName supportingContent = installedApp.originalPackageName
) )
} }
SettingsListItem( SettingsListItem(
headlineContent = stringResource(R.string.install_type), 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.ColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton 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.BundleInfo.Extensions.bundleInfoFlow
import app.revanced.manager.ui.model.SelectedApp 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.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.EventEffect import app.revanced.manager.util.EventEffect
import app.revanced.manager.util.Options 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.enabled
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.transparentListItemColors 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SelectedAppInfoScreen( fun SelectedAppInfoScreen(
onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit,
onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit, onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: SelectedAppInfoViewModel vm: SelectedAppInfoViewModel
@ -82,147 +78,119 @@ fun SelectedAppInfoScreen(
launcher.launch(intent) launcher.launch(intent)
} }
val navController = val error by vm.errorFlow.collectAsStateWithLifecycle(null)
rememberNavController<SelectedAppInfoDestination>(startDestination = SelectedAppInfoDestination.Main) Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.app_info),
onBackClick = onBackClick
)
},
floatingActionButton = {
if (error != null) return@Scaffold
NavBackHandler(controller = navController) HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.patch)) },
AnimatedNavHost(controller = navController) { destination -> icon = {
val error by vm.errorFlow.collectAsStateWithLifecycle(null) Icon(
when (destination) { Icons.Default.AutoFixHigh,
is SelectedAppInfoDestination.Main -> Scaffold( stringResource(R.string.patch)
topBar = {
AppTopBar(
title = stringResource(R.string.app_info),
onBackClick = onBackClick
) )
}, },
floatingActionButton = { onClick = patchClick@{
if (error != null) return@Scaffold if (selectedPatchCount == 0) {
context.toast(context.getString(R.string.no_patches_selected))
HapticExtendedFloatingActionButton( return@patchClick
text = { Text(stringResource(R.string.patch)) },
icon = {
Icon(
Icons.Default.AutoFixHigh,
stringResource(R.string.patch)
)
},
onClick = patchClick@{
if (selectedPatchCount == 0) {
context.toast(context.getString(R.string.no_patches_selected))
return@patchClick
}
onPatchClick(
vm.selectedApp,
patches,
vm.getOptionsFiltered(bundles)
)
}
)
}
) { paddingValues ->
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
if (vm.showSourceSelector) {
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
AppSourceSelectorDialog(
plugins = plugins,
installedApp = vm.installedAppData,
searchApp = SelectedApp.Search(
vm.packageName,
vm.desiredVersion
),
activeSearchJob = vm.activePluginAction,
hasRoot = vm.hasRoot,
onDismissRequest = vm::dismissSourceSelector,
onSelectPlugin = vm::searchUsingPlugin,
requiredVersion = requiredVersion,
onSelect = {
vm.selectedApp = it
vm.dismissSourceSelector()
}
)
}
ColumnWithScrollbar(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
Text(
version ?: stringResource(R.string.selected_app_meta_any_version),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
)
} }
onPatchClick(
PageItem( vm.selectedApp,
R.string.patch_selector_item, patches,
stringResource( vm.getOptionsFiltered(bundles)
R.string.patch_selector_item_description,
selectedPatchCount
),
onClick = {
navController.navigate(
SelectedAppInfoDestination.PatchesSelector(
vm.selectedApp,
vm.getCustomPatches(
bundles,
allowIncompatiblePatches
),
vm.options
)
)
}
)
PageItem(
R.string.apk_source_selector_item,
when (val app = vm.selectedApp) {
is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
is SelectedApp.Download -> stringResource(
R.string.apk_source_downloader,
plugins.find { it.packageName == app.data.pluginPackageName }?.name
?: app.data.pluginPackageName
)
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
},
onClick = {
vm.showSourceSelector()
}
)
error?.let {
Text(
stringResource(it.resourceId),
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(horizontal = 24.dp)
)
}
}
}
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,
)
) )
} }
) )
} }
) { paddingValues ->
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
if (vm.showSourceSelector) {
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
AppSourceSelectorDialog(
plugins = plugins,
installedApp = vm.installedAppData,
searchApp = SelectedApp.Search(
vm.packageName,
vm.desiredVersion
),
activeSearchJob = vm.activePluginAction,
hasRoot = vm.hasRoot,
onDismissRequest = vm::dismissSourceSelector,
onSelectPlugin = vm::searchUsingPlugin,
requiredVersion = requiredVersion,
onSelect = {
vm.selectedApp = it
vm.dismissSourceSelector()
}
)
}
ColumnWithScrollbar(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
Text(
version ?: stringResource(R.string.selected_app_meta_any_version),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
)
}
PageItem(
R.string.patch_selector_item,
stringResource(
R.string.patch_selector_item_description,
selectedPatchCount
),
onClick = {
onPatchSelectorClick(
vm.selectedApp,
vm.getCustomPatches(
bundles,
allowIncompatiblePatches
),
vm.options
)
}
)
PageItem(
R.string.apk_source_selector_item,
when (val app = vm.selectedApp) {
is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
is SelectedApp.Download -> stringResource(
R.string.apk_source_downloader,
plugins.find { it.packageName == app.data.pluginPackageName }?.name
?: app.data.pluginPackageName
)
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
},
onClick = {
vm.showSourceSelector()
}
)
error?.let {
Text(
stringResource(it.resourceId),
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(horizontal = 24.dp)
)
}
}
} }
} }

View File

@ -13,146 +13,64 @@ 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
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.destination.SettingsDestination import app.revanced.manager.ui.model.navigation.Settings
import app.revanced.manager.ui.screen.settings.*
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen private val settingsSections = listOf(
import app.revanced.manager.ui.screen.settings.update.UpdateScreen Triple(
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen R.string.general,
import app.revanced.manager.ui.viewmodel.SettingsViewModel R.string.general_description,
import dev.olshevski.navigation.reimagined.* Icons.Outlined.Settings
import org.koin.androidx.compose.koinViewModel ) to Settings.General,
import org.koin.core.parameter.parametersOf Triple(
R.string.updates,
R.string.updates_description,
Icons.Outlined.Update
) to Settings.Updates,
Triple(
R.string.downloads,
R.string.downloads_description,
Icons.Outlined.Download
) to Settings.Downloads,
Triple(
R.string.import_export,
R.string.import_export_description,
Icons.Outlined.SwapVert
) to Settings.ImportExport,
Triple(
R.string.advanced,
R.string.advanced_description,
Icons.Outlined.Tune
) to Settings.Advanced,
Triple(
R.string.about,
R.string.app_name,
Icons.Outlined.Info
) to Settings.About,
)
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> Unit) {
onBackClick: () -> Unit, Scaffold(
startDestination: SettingsDestination, topBar = {
viewModel: SettingsViewModel = koinViewModel() AppTopBar(
) { title = stringResource(R.string.settings),
val navController = rememberNavController(startDestination) onBackClick = onBackClick,
val backClick: () -> Unit = {
if (navController.backstack.entries.size == 1)
onBackClick()
else navController.pop()
}
val settingsSections = listOf(
Triple(
R.string.general,
R.string.general_description,
Icons.Outlined.Settings
) to SettingsDestination.General,
Triple(
R.string.updates,
R.string.updates_description,
Icons.Outlined.Update
) to SettingsDestination.Updates,
Triple(
R.string.downloads,
R.string.downloads_description,
Icons.Outlined.Download
) to SettingsDestination.Downloads,
Triple(
R.string.import_export,
R.string.import_export_description,
Icons.Outlined.SwapVert
) to SettingsDestination.ImportExport,
Triple(
R.string.advanced,
R.string.advanced_description,
Icons.Outlined.Tune
) to SettingsDestination.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
) )
}
is SettingsDestination.Advanced -> AdvancedSettingsScreen( ) { paddingValues ->
onBackClick = backClick ColumnWithScrollbar(
) modifier = Modifier
.padding(paddingValues)
is SettingsDestination.Updates -> UpdatesSettingsScreen( .fillMaxSize()
onBackClick = backClick, ) {
onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) }, settingsSections.forEach { (titleDescIcon, destination) ->
onUpdateClick = { navController.navigate(SettingsDestination.Update(false)) } SettingsListItem(
) modifier = Modifier.clickable { navigate(destination) },
headlineContent = stringResource(titleDescIcon.first),
is SettingsDestination.Downloads -> DownloadsSettingsScreen( supportingContent = stringResource(titleDescIcon.second),
onBackClick = backClick leadingContent = { Icon(titleDescIcon.third, null) }
) )
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 -> {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.settings),
onBackClick = backClick,
)
}
) { paddingValues ->
ColumnWithScrollbar(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
settingsSections.forEach { (titleDescIcon, destination) ->
SettingsListItem(
modifier = Modifier.clickable { navController.navigate(destination) },
headlineContent = stringResource(titleDescIcon.first),
supportingContent = stringResource(titleDescIcon.second),
leadingContent = { Icon(titleDescIcon.third, null) }
)
}
}
}
} }
} }
} }

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.AnimatedVisibility
import androidx.compose.animation.core.spring 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.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.settings.SettingsListItem 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
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon
import app.revanced.manager.util.openUrl import app.revanced.manager.util.openUrl
@ -47,9 +48,7 @@ import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun AboutSettingsScreen( fun AboutSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
onContributorsClick: () -> Unit, navigate: (Settings.Destination) -> Unit,
onLicensesClick: () -> Unit,
onDeveloperOptionsClick: () -> Unit,
viewModel: AboutViewModel = koinViewModel() viewModel: AboutViewModel = koinViewModel()
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -114,17 +113,17 @@ fun AboutSettingsScreen(
Triple( Triple(
stringResource(R.string.contributors), stringResource(R.string.contributors),
stringResource(R.string.contributors_description), stringResource(R.string.contributors_description),
third = onContributorsClick third = { navigate(Settings.Contributors) }
), ),
Triple( Triple(
stringResource(R.string.developer_options), stringResource(R.string.developer_options),
stringResource(R.string.developer_options_description), stringResource(R.string.developer_options_description),
third = onDeveloperOptionsClick third = { navigate(Settings.DeveloperOptions) }
), ),
Triple( Triple(
stringResource(R.string.opensource_licenses), stringResource(R.string.opensource_licenses),
stringResource(R.string.opensource_licenses_description), 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.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton 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.BooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.theme.Theme 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 import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun GeneralSettingsScreen( fun GeneralSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
viewModel: SettingsViewModel viewModel: GeneralSettingsViewModel = koinViewModel()
) { ) {
val prefs = viewModel.prefs val prefs = viewModel.prefs
val coroutineScope = viewModel.viewModelScope 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 app.revanced.manager.ui.theme.Theme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class SettingsViewModel( class GeneralSettingsViewModel(
val prefs: PreferencesManager val prefs: PreferencesManager
) : ViewModel() { ) : ViewModel() {
fun setTheme(theme: Theme) = viewModelScope.launch { fun setTheme(theme: Theme) = viewModelScope.launch {

View File

@ -32,15 +32,17 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
class InstalledAppInfoViewModel( class InstalledAppInfoViewModel(
val installedApp: InstalledApp packageName: String
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent {
private val app: Application by inject() private val context: Application by inject()
private val pm: PM by inject() private val pm: PM by inject()
private val installedAppRepository: InstalledAppRepository by inject() private val installedAppRepository: InstalledAppRepository by inject()
val rootInstaller: RootInstaller by inject() val rootInstaller: RootInstaller by inject()
lateinit var onBackClick: () -> Unit lateinit var onBackClick: () -> Unit
var installedApp: InstalledApp? by mutableStateOf(null)
private set
var appInfo: PackageInfo? by mutableStateOf(null) var appInfo: PackageInfo? by mutableStateOf(null)
private set private set
var appliedPatches: PatchSelection? by mutableStateOf(null) var appliedPatches: PatchSelection? by mutableStateOf(null)
@ -49,38 +51,48 @@ class InstalledAppInfoViewModel(
init { init {
viewModelScope.launch { 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 { fun mountOrUnmount() = viewModelScope.launch {
val pkgName = installedApp?.currentPackageName ?: return@launch
try { try {
if (isMounted) if (isMounted)
rootInstaller.unmount(installedApp.currentPackageName) rootInstaller.unmount(pkgName)
else else
rootInstaller.mount(installedApp.currentPackageName) rootInstaller.mount(pkgName)
} catch (e: Exception) { } catch (e: Exception) {
if (isMounted) { 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) Log.e(tag, "Failed to unmount", e)
} else { } 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) Log.e(tag, "Failed to mount", e)
} }
} finally { } finally {
isMounted = rootInstaller.isAppMounted(installedApp.currentPackageName) isMounted = rootInstaller.isAppMounted(pkgName)
} }
} }
fun uninstall() { fun uninstall() {
when (installedApp.installType) { val app = installedApp ?: return
InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName) when (app.installType) {
InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName)
InstallType.MOUNT -> viewModelScope.launch { InstallType.MOUNT -> viewModelScope.launch {
rootInstaller.uninstall(installedApp.currentPackageName) rootInstaller.uninstall(app.currentPackageName)
installedAppRepository.delete(installedApp) installedAppRepository.delete(app)
onBackClick() onBackClick()
} }
} }
@ -97,34 +109,22 @@ class InstalledAppInfoViewModel(
if (extraStatus == PackageInstaller.STATUS_SUCCESS) { if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
viewModelScope.launch { viewModelScope.launch {
installedAppRepository.delete(installedApp) installedApp?.let {
installedAppRepository.delete(it)
}
onBackClick() onBackClick()
} }
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) { } 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))
} }
} }
} }
} }
} }.also {
init {
viewModelScope.launch {
appInfo = withContext(Dispatchers.IO) {
pm.getPackageInfo(installedApp.currentPackageName)
}
}
viewModelScope.launch {
appliedPatches = withContext(Dispatchers.IO) {
installedAppRepository.getAppliedPatches(installedApp.currentPackageName)
}
}
ContextCompat.registerReceiver( ContextCompat.registerReceiver(
app, context,
uninstallBroadcastReceiver, it,
IntentFilter(UninstallService.APP_UNINSTALL_ACTION), IntentFilter(UninstallService.APP_UNINSTALL_ACTION),
ContextCompat.RECEIVER_NOT_EXPORTED ContextCompat.RECEIVER_NOT_EXPORTED
) )
@ -132,6 +132,6 @@ class InstalledAppInfoViewModel(
override fun onCleared() { override fun onCleared() {
super.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.plugin.downloader.UserInteractionException
import app.revanced.manager.service.InstallService import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService 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.InstallerModel
import app.revanced.manager.ui.model.ProgressKey import app.revanced.manager.ui.model.ProgressKey
import app.revanced.manager.ui.model.SelectedApp 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.Step
import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.StepProgressProvider 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.PM
import app.revanced.manager.util.saveableVar import app.revanced.manager.util.saveableVar
import app.revanced.manager.util.saver.snapshotStateListSaver import app.revanced.manager.util.saver.snapshotStateListSaver
@ -72,7 +72,7 @@ import java.time.Duration
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class) @OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
class PatcherViewModel( class PatcherViewModel(
private val input: Destination.Patcher private val input: Patcher.ViewModelParams
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel { ) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
private val app: Application by inject() private val app: Application by inject()
private val fs: Filesystem 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.bundleInfoFlow
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
import app.revanced.manager.ui.model.SelectedApp 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.Options
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.saver.Nullable import app.revanced.manager.util.saver.Nullable
@ -40,7 +41,7 @@ import kotlinx.coroutines.flow.map
@Stable @Stable
@OptIn(SavedStateHandleSaveableApi::class) @OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent { class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) : ViewModel(), KoinComponent {
private val app: Application = get() private val app: Application = get()
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
private val prefs: PreferencesManager = get() private val prefs: PreferencesManager = get()
@ -214,12 +215,6 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
private val selectionSaver: Saver<PersistentPatchSelection?, Nullable<PatchSelection>> = private val selectionSaver: Saver<PersistentPatchSelection?, Nullable<PatchSelection>> =
nullableSaver(persistentMapSaver(valueSaver = persistentSetSaver())) 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. // 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.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.ui.model.BundleInfo 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.BundleInfo.Extensions.toPatchSelection
import app.revanced.manager.ui.model.SelectedApp 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.Options
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
@ -57,7 +59,9 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class) @OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { class SelectedAppInfoViewModel(
input: SelectedApplicationInfo.ViewModelParams
) : ViewModel(), KoinComponent {
private val app: Application = get() private val app: Application = get()
val bundlesRepo: PatchBundleRepository = get() val bundlesRepo: PatchBundleRepository = get()
private val bundleRepository: 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 if (!suggestedVersionSafeguard) return@combine null
suggestedVersions[input.app.packageName] suggestedVersions[input.app.packageName]
@ -264,17 +271,15 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
): PatchSelection? = ): PatchSelection? =
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported) (selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
fun updateConfiguration( fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch {
selection: PatchSelection?, val bundles = bundlesRepo.bundleInfoFlow(packageName, selectedApp.version).first()
options: Options,
bundles: List<BundleInfo>
) {
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
val filteredOptions = options.filtered(bundles) val filteredOptions = options.filtered(bundles)
this.options = filteredOptions this@SelectedAppInfoViewModel.options = filteredOptions
if (!persistConfiguration) return if (!persistConfiguration) return@launch
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
selection?.let { selectionRepository.updateSelection(packageName, it) } selection?.let { selectionRepository.updateSelection(packageName, it) }
?: selectionRepository.clearSelection(packageName) ?: 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) { enum class Error(@StringRes val resourceId: Int) {
NoPlugins(R.string.downloader_no_plugins_available) 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.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo 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.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.MainThread import androidx.annotation.MainThread

View File

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