feat: selected app info page (#1395)

This commit is contained in:
Ax333l 2023-10-19 21:44:50 +02:00 committed by GitHub
parent 7ba00cafd9
commit c3af6acb2c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 779 additions and 309 deletions

View file

@ -19,16 +19,17 @@ import androidx.compose.runtime.setValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.screen.AppInfoScreen import app.revanced.manager.ui.screen.InstalledAppInfoScreen
import app.revanced.manager.ui.screen.AppSelectorScreen import app.revanced.manager.ui.screen.AppSelectorScreen
import app.revanced.manager.ui.screen.DashboardScreen import app.revanced.manager.ui.screen.DashboardScreen
import app.revanced.manager.ui.screen.InstallerScreen import app.revanced.manager.ui.screen.InstallerScreen
import app.revanced.manager.ui.screen.PatchesSelectorScreen import app.revanced.manager.ui.screen.SelectedAppInfoScreen
import app.revanced.manager.ui.screen.SettingsScreen import app.revanced.manager.ui.screen.SettingsScreen
import app.revanced.manager.ui.screen.VersionSelectorScreen import app.revanced.manager.ui.screen.VersionSelectorScreen
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.util.tag import app.revanced.manager.util.tag
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import dev.olshevski.navigation.reimagined.AnimatedNavHost import dev.olshevski.navigation.reimagined.AnimatedNavHost
@ -37,9 +38,9 @@ import dev.olshevski.navigation.reimagined.navigate
import dev.olshevski.navigation.reimagined.pop import dev.olshevski.navigation.reimagined.pop
import dev.olshevski.navigation.reimagined.popUpTo import dev.olshevski.navigation.reimagined.popUpTo
import dev.olshevski.navigation.reimagined.rememberNavController import dev.olshevski.navigation.reimagined.rememberNavController
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel as getComposeViewModel
import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ExperimentalAnimationApi @ExperimentalAnimationApi
@ -48,7 +49,7 @@ class MainActivity : ComponentActivity() {
installSplashScreen() installSplashScreen()
val vm: MainViewModel = getActivityViewModel() val vm: MainViewModel = getAndroidViewModel()
setContent { setContent {
val theme by vm.prefs.theme.getAsState() val theme by vm.prefs.theme.getAsState()
@ -102,7 +103,7 @@ class MainActivity : ComponentActivity() {
} }
legacyActivityState = LegacyActivity.LAUNCHED legacyActivityState = LegacyActivity.LAUNCHED
} else if (legacyActivityState == LegacyActivity.FAILED){ } else if (legacyActivityState == LegacyActivity.FAILED) {
AutoUpdatesDialog(vm::applyAutoUpdatePrefs) AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
} }
} }
@ -114,15 +115,26 @@ class MainActivity : ComponentActivity() {
is Destination.Dashboard -> DashboardScreen( is Destination.Dashboard -> DashboardScreen(
onSettingsClick = { navController.navigate(Destination.Settings) }, onSettingsClick = { navController.navigate(Destination.Settings) },
onAppSelectorClick = { navController.navigate(Destination.AppSelector) }, onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
onAppClick = { installedApp -> navController.navigate(Destination.ApplicationInfo(installedApp)) } onAppClick = { installedApp ->
navController.navigate(
Destination.InstalledApplicationInfo(
installedApp
)
)
}
) )
is Destination.ApplicationInfo -> AppInfoScreen( is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen(
onPatchClick = { packageName, patchesSelection -> onPatchClick = { packageName, patchesSelection ->
navController.navigate(Destination.VersionSelector(packageName, patchesSelection)) navController.navigate(
Destination.VersionSelector(
packageName,
patchesSelection
)
)
}, },
onBackClick = { navController.pop() }, onBackClick = { navController.pop() },
viewModel = getViewModel { parametersOf(destination.installedApp) } viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
) )
is Destination.Settings -> SettingsScreen( is Destination.Settings -> SettingsScreen(
@ -131,7 +143,13 @@ class MainActivity : ComponentActivity() {
is Destination.AppSelector -> AppSelectorScreen( is Destination.AppSelector -> AppSelectorScreen(
onAppClick = { navController.navigate(Destination.VersionSelector(it)) }, onAppClick = { navController.navigate(Destination.VersionSelector(it)) },
onStorageClick = { navController.navigate(Destination.PatchesSelector(it)) }, onStorageClick = {
navController.navigate(
Destination.SelectedApplicationInfo(
it
)
)
},
onBackClick = { navController.pop() } onBackClick = { navController.pop() }
) )
@ -139,32 +157,42 @@ class MainActivity : ComponentActivity() {
onBackClick = { navController.pop() }, onBackClick = { navController.pop() },
onAppClick = { selectedApp -> onAppClick = { selectedApp ->
navController.navigate( navController.navigate(
Destination.PatchesSelector( Destination.SelectedApplicationInfo(
selectedApp, selectedApp,
destination.patchesSelection,
)
)
},
viewModel = getComposeViewModel {
parametersOf(
destination.packageName,
destination.patchesSelection
)
}
)
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
onPatchClick = { app, patches, options ->
navController.navigate(
Destination.Installer(
app, patches, options
)
)
},
onBackClick = navController::pop,
vm = getComposeViewModel {
parametersOf(
SelectedAppInfoViewModel.Params(
destination.selectedApp,
destination.patchesSelection destination.patchesSelection
) )
) )
}, }
viewModel = getViewModel { parametersOf(destination.packageName, destination.patchesSelection) }
)
is Destination.PatchesSelector -> PatchesSelectorScreen(
onBackClick = { navController.pop() },
onPatchClick = { patches, options ->
navController.navigate(
Destination.Installer(
destination.selectedApp,
patches,
options
)
)
},
vm = getViewModel { parametersOf(destination) }
) )
is Destination.Installer -> InstallerScreen( is Destination.Installer -> InstallerScreen(
onBackClick = { navController.popUpTo { it is Destination.Dashboard } }, onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
vm = getViewModel { parametersOf(destination) } vm = getComposeViewModel { parametersOf(destination) }
) )
} }
} }

View file

@ -7,6 +7,7 @@ import org.koin.dsl.module
val viewModelModule = module { val viewModelModule = module {
viewModelOf(::MainViewModel) viewModelOf(::MainViewModel)
viewModelOf(::DashboardViewModel) viewModelOf(::DashboardViewModel)
viewModelOf(::SelectedAppInfoViewModel)
viewModelOf(::PatchesSelectorViewModel) viewModelOf(::PatchesSelectorViewModel)
viewModelOf(::SettingsViewModel) viewModelOf(::SettingsViewModel)
viewModelOf(::AdvancedSettingsViewModel) viewModelOf(::AdvancedSettingsViewModel)
@ -19,5 +20,5 @@ val viewModelModule = module {
viewModelOf(::ContributorViewModel) viewModelOf(::ContributorViewModel)
viewModelOf(::DownloadsViewModel) viewModelOf(::DownloadsViewModel)
viewModelOf(::InstalledAppsViewModel) viewModelOf(::InstalledAppsViewModel)
viewModelOf(::AppInfoViewModel) viewModelOf(::InstalledAppInfoViewModel)
} }

View file

@ -0,0 +1,39 @@
package app.revanced.manager.ui.component
import android.content.pm.PackageInfo
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun AppInfo(appInfo: PackageInfo?, placeholderLabel: String? = null, extraContent: @Composable () -> Unit = {}) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AppIcon(
appInfo,
contentDescription = null,
modifier = Modifier
.size(100.dp)
.padding(bottom = 5.dp)
)
AppLabel(
appInfo,
modifier = Modifier.padding(top = 16.dp),
style = MaterialTheme.typography.titleLarge,
defaultText = placeholderLabel
)
extraContent()
}
}

View file

@ -14,7 +14,7 @@ sealed interface Destination : Parcelable {
object Dashboard : Destination object Dashboard : Destination
@Parcelize @Parcelize
data class ApplicationInfo(val installedApp: InstalledApp) : Destination data class InstalledApplicationInfo(val installedApp: InstalledApp) : Destination
@Parcelize @Parcelize
object AppSelector : Destination object AppSelector : Destination
@ -26,7 +26,7 @@ sealed interface Destination : Parcelable {
data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination
@Parcelize @Parcelize
data class PatchesSelector(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
@Parcelize @Parcelize
data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination

View file

@ -0,0 +1,19 @@
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.PatchesSelection
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: PatchesSelection?, val options: @RawValue Options) : SelectedAppInfoDestination
@Parcelize
data object VersionSelector: SelectedAppInfoDestination
}

View file

@ -0,0 +1,82 @@
package app.revanced.manager.ui.model
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.flatMapLatestAndCombine
import kotlinx.coroutines.flow.map
/**
* A data class that contains patch bundle metadata for use by UI code.
*/
data class BundleInfo(
val name: String,
val uid: Int,
val supported: List<PatchInfo>,
val unsupported: List<PatchInfo>,
val universal: List<PatchInfo>
) {
val all = sequence {
yieldAll(supported)
yieldAll(unsupported)
yieldAll(universal)
}
val patchCount get() = supported.size + unsupported.size + universal.size
fun patchSequence(allowUnsupported: Boolean) = if (allowUnsupported) {
all
} else {
sequence {
yieldAll(supported)
yieldAll(universal)
}
}
companion object Extensions {
inline fun Iterable<BundleInfo>.toPatchSelection(allowUnsupported: Boolean, condition: (Int, PatchInfo) -> Boolean): PatchesSelection = this.associate { bundle ->
val patches =
bundle.patchSequence(allowUnsupported)
.mapNotNullTo(mutableSetOf()) { patch ->
patch.name.takeIf {
condition(
bundle.uid,
patch
)
}
}
bundle.uid to patches
}
fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String) =
sources.flatMapLatestAndCombine(
combiner = { it.filterNotNull() }
) { source ->
// Regenerate bundle information whenever this source updates.
source.state.map { state ->
val bundle = state.patchBundleOrNull() ?: return@map null
val supported = mutableListOf<PatchInfo>()
val unsupported = mutableListOf<PatchInfo>()
val universal = mutableListOf<PatchInfo>()
bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
val targetList = when {
it.compatiblePackages == null -> universal
it.supportsVersion(
packageName,
version
) -> supported
else -> unsupported
}
targetList.add(it)
}
BundleInfo(source.name, source.uid, supported, unsupported, universal)
}
}
}
}

View file

@ -41,18 +41,19 @@ import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.ui.component.AppIcon import app.revanced.manager.ui.component.AppIcon
import app.revanced.manager.ui.component.AppInfo
import app.revanced.manager.ui.component.AppLabel import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.SegmentedButton import app.revanced.manager.ui.component.SegmentedButton
import app.revanced.manager.ui.viewmodel.AppInfoViewModel import app.revanced.manager.ui.viewmodel.InstalledAppInfoViewModel
import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.PatchesSelection
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppInfoScreen( fun InstalledAppInfoScreen(
onPatchClick: (packageName: String, patchesSelection: PatchesSelection) -> Unit, onPatchClick: (packageName: String, patchesSelection: PatchesSelection) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
viewModel: AppInfoViewModel viewModel: InstalledAppInfoViewModel
) { ) {
SideEffect { SideEffect {
viewModel.onBackClick = onBackClick viewModel.onBackClick = onBackClick
@ -80,27 +81,8 @@ fun AppInfoScreen(
.padding(paddingValues) .padding(paddingValues)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
Column( AppInfo(viewModel.appInfo) {
modifier = Modifier Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AppIcon(
viewModel.appInfo,
contentDescription = null,
modifier = Modifier
.size(100.dp)
.padding(bottom = 5.dp)
)
AppLabel(
viewModel.appInfo,
style = MaterialTheme.typography.titleLarge,
defaultText = null
)
Text(viewModel.installedApp.version, style = MaterialTheme.typography.bodySmall)
if (viewModel.installedApp.installType == InstallType.ROOT) { if (viewModel.installedApp.installType == InstallType.ROOT) {
Text( Text(

View file

@ -15,16 +15,15 @@ import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Restore import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.WarningAmber import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
@ -42,6 +41,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -57,7 +57,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchInfo
@ -65,7 +64,6 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.Countdown import app.revanced.manager.ui.component.Countdown
import app.revanced.manager.ui.component.patches.OptionItem import app.revanced.manager.ui.component.patches.OptionItem
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.BaseSelectionMode
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
@ -77,7 +75,7 @@ import org.koin.compose.rememberKoinInject
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
fun PatchesSelectorScreen( fun PatchesSelectorScreen(
onPatchClick: (PatchesSelection, Options) -> Unit, onSave: (PatchesSelection?, Options) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: PatchesSelectorViewModel vm: PatchesSelectorViewModel
) { ) {
@ -93,10 +91,10 @@ fun PatchesSelectorScreen(
mutableStateOf(null) mutableStateOf(null)
} }
var showBottomSheet by rememberSaveable { mutableStateOf(false) } var showBottomSheet by rememberSaveable { mutableStateOf(false) }
var showPatchButton by remember { mutableStateOf(true) } val showPatchButton by remember {
LaunchedEffect(Unit) { derivedStateOf { vm.selectionIsValid(bundles) }
showPatchButton = vm.isSelectionNotEmpty()
} }
if (showBottomSheet) { if (showBottomSheet) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { onDismissRequest = {
@ -140,39 +138,12 @@ fun PatchesSelectorScreen(
) )
} }
} }
Divider()
ListItem(
modifier = Modifier
.fillMaxWidth()
.clickable(
enabled = vm.hasPreviousSelection,
onClick = vm::switchBaseSelectionMode
),
leadingContent = {
Checkbox(
checked = vm.baseSelectionMode == BaseSelectionMode.PREVIOUS,
onCheckedChange = {
vm.switchBaseSelectionMode()
},
enabled = vm.hasPreviousSelection
)
},
headlineContent = {
Text(
"Use previous selection",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
}
)
} }
} }
if (vm.compatibleVersions.isNotEmpty()) if (vm.compatibleVersions.isNotEmpty())
UnsupportedDialog( UnsupportedDialog(
appVersion = vm.input.selectedApp.version, appVersion = vm.appVersion,
supportedVersions = vm.compatibleVersions, supportedVersions = vm.compatibleVersions,
onDismissRequest = vm::dismissDialogs onDismissRequest = vm::dismissDialogs
) )
@ -194,8 +165,6 @@ fun PatchesSelectorScreen(
) )
} }
val allowExperimental by vm.allowExperimental.getAsState()
fun LazyListScope.patchList( fun LazyListScope.patchList(
uid: Int, uid: Int,
patches: List<PatchInfo>, patches: List<PatchInfo>,
@ -227,17 +196,10 @@ fun PatchesSelectorScreen(
if (vm.selectionWarningEnabled) { if (vm.selectionWarningEnabled) {
vm.pendingSelectionAction = { vm.pendingSelectionAction = {
vm.togglePatch(uid, patch) vm.togglePatch(uid, patch)
vm.viewModelScope.launch {
showPatchButton = vm.isSelectionNotEmpty()
}
} }
} else { } else {
vm.togglePatch(uid, patch) vm.togglePatch(uid, patch)
vm.viewModelScope.launch {
showPatchButton = vm.isSelectionNotEmpty()
}
} }
}, },
supported = supported supported = supported
) )
@ -292,7 +254,7 @@ fun PatchesSelectorScreen(
) )
} }
if (!allowExperimental) return@LazyColumn if (!vm.allowExperimental) return@LazyColumn
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.unsupported.searched(), patches = bundle.unsupported.searched(),
@ -332,22 +294,19 @@ fun PatchesSelectorScreen(
) )
}, },
floatingActionButton = { floatingActionButton = {
if(showPatchButton) { if (!showPatchButton) return@Scaffold
ExtendedFloatingActionButton(
text = { ExtendedFloatingActionButton(
Text(stringResource(R.string.patch)) text = { Text(stringResource(R.string.save)) },
}, icon = { Icon(Icons.Outlined.Save, null) },
icon = { Icon(Icons.Default.Build, null) }, onClick = {
onClick = { // TODO: only allow this if all required options have been set.
// TODO: only allow this if all required options have been set. composableScope.launch {
composableScope.launch { vm.saveSelection()
val selection = vm.getSelection() onSave(vm.getCustomSelection(), vm.getOptions())
vm.saveSelection(selection).join()
onPatchClick(selection, vm.getOptions())
}
} }
) }
} )
} }
) { paddingValues -> ) { paddingValues ->
Column( Column(
@ -407,7 +366,7 @@ fun PatchesSelectorScreen(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.unsupported, patches = bundle.unsupported,
filterFlag = SHOW_UNSUPPORTED, filterFlag = SHOW_UNSUPPORTED,
supported = allowExperimental supported = vm.allowExperimental
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.unsupported_patches), title = stringResource(R.string.unsupported_patches),

View file

@ -0,0 +1,218 @@
package app.revanced.manager.ui.screen
import android.content.pm.PackageInfo
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowRight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
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.ui.component.AppInfo
import app.revanced.manager.ui.component.AppTopBar
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.Options
import app.revanced.manager.util.PatchesSelection
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.rememberNavController
import org.koin.androidx.compose.getViewModel
import org.koin.core.parameter.parametersOf
@Composable
fun SelectedAppInfoScreen(
onPatchClick: (SelectedApp, PatchesSelection, Options) -> Unit,
onBackClick: () -> Unit,
vm: SelectedAppInfoViewModel
) {
val bundles by remember(vm.selectedApp.packageName, vm.selectedApp.version) {
vm.bundlesRepo.bundleInfoFlow(vm.selectedApp.packageName, vm.selectedApp.version)
}.collectAsStateWithLifecycle(initialValue = emptyList())
val allowExperimental by vm.prefs.allowExperimental.getAsState()
val patches by remember {
derivedStateOf {
vm.getPatches(bundles, allowExperimental)
}
}
val selectedPatchCount by remember {
derivedStateOf {
patches.values.sumOf { it.size }
}
}
val availablePatchCount by remember {
derivedStateOf {
bundles.sumOf { it.patchCount }
}
}
val navController =
rememberNavController<SelectedAppInfoDestination>(startDestination = SelectedAppInfoDestination.Main)
NavBackHandler(controller = navController)
AnimatedNavHost(controller = navController) { destination ->
when (destination) {
is SelectedAppInfoDestination.Main -> SelectedAppInfoScreen(
onPatchClick = {
onPatchClick(
vm.selectedApp,
patches,
vm.patchOptions
)
},
onPatchSelectorClick = {
navController.navigate(
SelectedAppInfoDestination.PatchesSelector(
vm.selectedApp,
vm.getCustomPatches(
bundles,
allowExperimental
),
vm.patchOptions
)
)
},
onVersionSelectorClick = {
navController.navigate(SelectedAppInfoDestination.VersionSelector)
},
onBackClick = onBackClick,
availablePatchCount = availablePatchCount,
selectedPatchCount = selectedPatchCount,
packageName = vm.selectedApp.packageName,
version = vm.selectedApp.version,
packageInfo = vm.selectedAppInfo,
)
is SelectedAppInfoDestination.VersionSelector -> VersionSelectorScreen(
onBackClick = navController::pop,
onAppClick = {
vm.setSelectedApp(it)
navController.pop()
},
viewModel = getViewModel { parametersOf(vm.selectedApp.packageName) }
)
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
onSave = { patches, options ->
vm.setCustomPatches(patches)
vm.patchOptions = options
navController.pop()
},
onBackClick = navController::pop,
vm = getViewModel {
parametersOf(
PatchesSelectorViewModel.Params(
destination.app,
destination.currentSelection,
destination.options
)
)
}
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SelectedAppInfoScreen(
onPatchClick: () -> Unit,
onPatchSelectorClick: () -> Unit,
onVersionSelectorClick: () -> Unit,
onBackClick: () -> Unit,
availablePatchCount: Int,
selectedPatchCount: Int,
packageName: String,
version: String,
packageInfo: PackageInfo?,
) {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.app_info),
onBackClick = onBackClick
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
AppInfo(packageInfo, placeholderLabel = packageName) {
Text(
stringResource(R.string.selected_app_meta, version, availablePatchCount),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
)
}
PageItem(R.string.patch, stringResource(R.string.patch_item_description), onPatchClick)
Text(
stringResource(R.string.advanced),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
)
PageItem(
R.string.patch_selector_item,
stringResource(R.string.patch_selector_item_description, selectedPatchCount),
onPatchSelectorClick
)
PageItem(
R.string.version_selector_item,
stringResource(R.string.version_selector_item_description, version),
onVersionSelectorClick
)
}
}
}
@Composable
private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) {
ListItem(
modifier = Modifier
.clickable(onClick = onClick)
.padding(start = 8.dp),
headlineContent = {
Text(
stringResource(title),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge
)
},
supportingContent = {
Text(
description,
color = MaterialTheme.colorScheme.outline,
style = MaterialTheme.typography.bodyMedium
)
},
trailingContent = {
Icon(Icons.Outlined.ArrowRight, null)
}
)
}

View file

@ -30,7 +30,7 @@ import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
class AppInfoViewModel( class InstalledAppInfoViewModel(
val installedApp: InstalledApp val installedApp: InstalledApp
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent {
private val app: Application by inject() private val app: Application by inject()

View file

@ -2,8 +2,8 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
@ -20,66 +20,46 @@ import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.destination.Destination 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.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.flatMapLatestAndCombine import app.revanced.manager.util.saver.nullableSaver
import app.revanced.manager.util.saver.persistentMapSaver
import app.revanced.manager.util.saver.persistentSetSaver
import app.revanced.manager.util.saver.snapshotStateMapSaver import app.revanced.manager.util.saver.snapshotStateMapSaver
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import kotlinx.collections.immutable.*
import kotlinx.coroutines.withContext
import java.util.Optional
@Stable @Stable
@OptIn(SavedStateHandleSaveableApi::class) @OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel( class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
val input: Destination.PatchesSelector
) : ViewModel(), KoinComponent {
private val app: Application = get() private val app: Application = get()
private val selectionRepository: PatchSelectionRepository = get() private val selectionRepository: PatchSelectionRepository = get()
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
private val prefs: PreferencesManager = get() private val prefs: PreferencesManager = get()
private val packageName = input.selectedApp.packageName private val packageName = input.app.packageName
val appVersion = input.app.version
var pendingSelectionAction by mutableStateOf<(() -> Unit)?>(null) var pendingSelectionAction by mutableStateOf<(() -> Unit)?>(null)
// TODO: this should be hoisted to the parent screen
var selectionWarningEnabled by mutableStateOf(true) var selectionWarningEnabled by mutableStateOf(true)
private set private set
val allowExperimental = get<PreferencesManager>().allowExperimental val allowExperimental = get<PreferencesManager>().allowExperimental.getBlocking()
val bundlesFlow = get<PatchBundleRepository>().sources.flatMapLatestAndCombine( val bundlesFlow =
combiner = { it.filterNotNull() } get<PatchBundleRepository>().bundleInfoFlow(packageName, input.app.version)
) { source ->
// Regenerate bundle information whenever this source updates.
source.state.map { state ->
val bundle = state.patchBundleOrNull() ?: return@map null
val supported = mutableListOf<PatchInfo>()
val unsupported = mutableListOf<PatchInfo>()
val universal = mutableListOf<PatchInfo>()
bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
val targetList = when {
it.compatiblePackages == null -> universal
it.supportsVersion(
input.selectedApp.packageName,
input.selectedApp.version
) -> supported
else -> unsupported
}
targetList.add(it)
}
BundleInfo(source.name, source.uid, bundle.patches, supported, unsupported, universal)
}
}
init { init {
viewModelScope.launch { viewModelScope.launch {
@ -88,63 +68,28 @@ class PatchesSelectorViewModel(
return@launch return@launch
} }
val experimental = allowExperimental.get() fun BundleInfo.hasDefaultPatches() = patchSequence(allowExperimental).any { it.include }
fun BundleInfo.hasDefaultPatches(): Boolean {
return if (experimental) {
all.asSequence()
} else {
sequence {
yieldAll(supported)
yieldAll(universal)
}
}.any { it.include }
}
// Don't show the warning if there are no default patches. // Don't show the warning if there are no default patches.
selectionWarningEnabled = bundlesFlow.first().any(BundleInfo::hasDefaultPatches) selectionWarningEnabled = bundlesFlow.first().any(BundleInfo::hasDefaultPatches)
} }
} }
var baseSelectionMode by mutableStateOf(BaseSelectionMode.DEFAULT)
private set
private val previousPatchesSelection: SnapshotStateMap<Int, Set<String>> = mutableStateMapOf()
init {
viewModelScope.launch(Dispatchers.Default) { loadPreviousSelection() }
}
val hasPreviousSelection by derivedStateOf {
previousPatchesSelection.filterValues(Set<String>::isNotEmpty).isNotEmpty()
}
private var hasModifiedSelection = false private var hasModifiedSelection = false
private var customPatchesSelection: PersistentPatchesSelection? by savedStateHandle.saveable(
key = "selection",
stateSaver = patchesSaver,
) {
mutableStateOf(input.currentSelection?.toPersistentPatchesSelection())
}
private val explicitPatchesSelection: SnapshotExplicitPatchesSelection by savedStateHandle.saveable( private val patchOptions: PersistentOptions by savedStateHandle.saveable(
saver = explicitPatchesSelectionSaver,
init = ::mutableStateMapOf
)
private val patchOptions: SnapshotOptions by savedStateHandle.saveable(
saver = optionsSaver, saver = optionsSaver,
init = ::mutableStateMapOf ) {
) // Convert Options to PersistentOptions
input.options.mapValuesTo(mutableStateMapOf()) { (_, allPatches) ->
private val selectors by derivedStateOf<Array<Selector>> { allPatches.mapValues { (_, options) -> options.toPersistentMap() }.toPersistentMap()
arrayOf( }
// Patches that were explicitly selected
{ bundle, patch ->
explicitPatchesSelection[bundle]?.get(patch.name)
},
// The fallback selection.
when (baseSelectionMode) {
BaseSelectionMode.DEFAULT -> ({ _, patch -> patch.include })
BaseSelectionMode.PREVIOUS -> ({ bundle, patch ->
previousPatchesSelection[bundle]?.contains(patch.name) ?: false
})
}
)
} }
/** /**
@ -154,52 +99,39 @@ class PatchesSelectorViewModel(
val compatibleVersions = mutableStateListOf<String>() val compatibleVersions = mutableStateListOf<String>()
var filter by mutableStateOf(SHOW_SUPPORTED or SHOW_UNIVERSAL or SHOW_UNSUPPORTED) var filter by mutableIntStateOf(SHOW_SUPPORTED or SHOW_UNIVERSAL or SHOW_UNSUPPORTED)
private set private set
private suspend fun loadPreviousSelection() { private suspend fun generateDefaultSelection(): PersistentPatchesSelection {
val selection = (input.patchesSelection ?: selectionRepository.getSelection( val bundles = bundlesFlow.first()
packageName val generatedSelection =
)).mapValues { (_, value) -> value.toSet() } bundles.toPatchSelection(allowExperimental) { _, patch -> patch.include }
withContext(Dispatchers.Main) { return generatedSelection.toPersistentPatchesSelection()
previousPatchesSelection.putAll(selection) }
fun selectionIsValid(bundles: List<BundleInfo>) = bundles.any { bundle ->
bundle.patchSequence(allowExperimental).any { patch ->
isSelected(bundle.uid, patch)
} }
} }
fun switchBaseSelectionMode() = viewModelScope.launch { fun isSelected(bundle: Int, patch: PatchInfo) = customPatchesSelection?.let { selection ->
baseSelectionMode = if (baseSelectionMode == BaseSelectionMode.DEFAULT) { selection[bundle]?.contains(patch.name) ?: false
BaseSelectionMode.PREVIOUS } ?: patch.include
} else {
BaseSelectionMode.DEFAULT
}
}
private suspend fun patchesAvailable(bundle: BundleInfo): List<PatchInfo> {
val patches = (bundle.supported + bundle.universal).toMutableList()
val removeUnsupported = !allowExperimental.get()
if (!removeUnsupported) patches += bundle.unsupported
return patches
}
suspend fun isSelectionNotEmpty() =
bundlesFlow.first().any { bundle ->
patchesAvailable(bundle).any { patch ->
isSelected(bundle.uid, patch)
}
}
private fun getOrCreateSelection(bundle: Int) =
explicitPatchesSelection.getOrPut(bundle, ::mutableStateMapOf)
fun isSelected(bundle: Int, patch: PatchInfo) =
selectors.firstNotNullOf { fn -> fn(bundle, patch) }
fun togglePatch(bundle: Int, patch: PatchInfo) {
val patches = getOrCreateSelection(bundle)
fun togglePatch(bundle: Int, patch: PatchInfo) = viewModelScope.launch {
hasModifiedSelection = true hasModifiedSelection = true
patches[patch.name] = !isSelected(bundle, patch)
val selection = customPatchesSelection ?: generateDefaultSelection()
val newPatches = selection[bundle]?.let { patches ->
if (patch.name in patches)
patches.remove(patch.name)
else
patches.add(patch.name)
} ?: persistentSetOf(patch.name)
customPatchesSelection = selection.put(bundle, newPatches)
} }
fun confirmSelectionWarning(dismissPermanently: Boolean) { fun confirmSelectionWarning(dismissPermanently: Boolean) {
@ -221,46 +153,39 @@ class PatchesSelectorViewModel(
fun reset() { fun reset() {
patchOptions.clear() patchOptions.clear()
baseSelectionMode = BaseSelectionMode.DEFAULT customPatchesSelection = null
explicitPatchesSelection.clear()
hasModifiedSelection = false hasModifiedSelection = false
app.toast(app.getString(R.string.patch_selection_reset_toast)) app.toast(app.getString(R.string.patch_selection_reset_toast))
} }
suspend fun getSelection(): PatchesSelection { fun getCustomSelection(): PatchesSelection? {
val bundles = bundlesFlow.first() // Convert persistent collections to standard hash collections because persistent collections are not parcelable.
val removeUnsupported = !allowExperimental.get()
return bundles.associate { bundle -> return customPatchesSelection?.mapValues { (_, v) -> v.toSet() }
val included =
bundle.all.filter { isSelected(bundle.uid, it) }.map { it.name }.toMutableSet()
if (removeUnsupported) {
val unsupported = bundle.unsupported.map { it.name }.toSet()
included.removeAll(unsupported)
}
bundle.uid to included
}
} }
suspend fun saveSelection(selection: PatchesSelection) = fun getOptions(): Options {
viewModelScope.launch(Dispatchers.Default) { // Convert the collection for the same reasons as in getCustomSelection()
when {
hasModifiedSelection -> selectionRepository.updateSelection(packageName, selection)
baseSelectionMode == BaseSelectionMode.DEFAULT -> selectionRepository.clearSelection(
packageName
)
else -> {} return patchOptions.mapValues { (_, allPatches) -> allPatches.mapValues { (_, options) -> options.toMap() } }
} }
}
suspend fun saveSelection() = withContext(Dispatchers.Default) {
customPatchesSelection?.let { selectionRepository.updateSelection(packageName, it) }
?: selectionRepository.clearSelection(packageName)
}
fun getOptions(): Options = patchOptions
fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name) fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) { fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) {
patchOptions.getOrCreate(bundle).getOrCreate(patch.name)[key] = value // All patches
val patchesToOpts = patchOptions.getOrElse(bundle, ::persistentMapOf)
// The key-value options of an individual patch
val patchToOpts = patchesToOpts
.getOrElse(patch.name, ::persistentMapOf)
.put(key, value)
patchOptions[bundle] = patchesToOpts.put(patch.name, patchToOpts)
} }
fun resetOptions(bundle: Int, patch: PatchInfo) { fun resetOptions(bundle: Int, patch: PatchInfo) {
@ -274,7 +199,7 @@ class PatchesSelectorViewModel(
fun openUnsupportedDialog(unsupportedPatches: List<PatchInfo>) { fun openUnsupportedDialog(unsupportedPatches: List<PatchInfo>) {
compatibleVersions.addAll(unsupportedPatches.flatMap { patch -> compatibleVersions.addAll(unsupportedPatches.flatMap { patch ->
patch.compatiblePackages?.find { it.packageName == input.selectedApp.packageName }?.versions.orEmpty() patch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty()
}) })
} }
@ -287,50 +212,28 @@ class PatchesSelectorViewModel(
const val SHOW_UNIVERSAL = 2 // 2^1 const val SHOW_UNIVERSAL = 2 // 2^1
const val SHOW_UNSUPPORTED = 4 // 2^2 const val SHOW_UNSUPPORTED = 4 // 2^2
private fun <K, K2, V> SnapshotStateMap<K, SnapshotStateMap<K2, V>>.getOrCreate(key: K) = private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver(
getOrPut(key, ::mutableStateMapOf)
private val optionsSaver: Saver<SnapshotOptions, Options> = snapshotStateMapSaver(
// Patch name -> Options // Patch name -> Options
valueSaver = snapshotStateMapSaver( valueSaver = persistentMapSaver(
// Option key -> Option value // Option key -> Option value
valueSaver = snapshotStateMapSaver() valueSaver = persistentMapSaver()
) )
) )
private val explicitPatchesSelectionSaver: Saver<SnapshotExplicitPatchesSelection, ExplicitPatchesSelection> = private val patchesSaver: Saver<PersistentPatchesSelection?, Optional<PatchesSelection>> =
snapshotStateMapSaver(valueSaver = snapshotStateMapSaver()) nullableSaver(persistentMapSaver(valueSaver = persistentSetSaver()))
} }
/** data class Params(
* An enum for controlling the behavior of the selector. val app: SelectedApp,
*/ val currentSelection: PatchesSelection?,
enum class BaseSelectionMode { val options: Options,
/**
* Selection is determined by the [PatchInfo.include] field.
*/
DEFAULT,
/**
* Selection is determined by what the user selected previously.
* Any patch that is not part of the previous selection will be deselected.
*/
PREVIOUS
}
data class BundleInfo(
val name: String,
val uid: Int,
val all: List<PatchInfo>,
val supported: List<PatchInfo>,
val unsupported: List<PatchInfo>,
val universal: List<PatchInfo>
) )
} }
private typealias Selector = (Int, PatchInfo) -> Boolean? // Versions of other types, but utilizing persistent/observable collection types.
private typealias ExplicitPatchesSelection = Map<Int, Map<String, Boolean>> private typealias PersistentOptions = SnapshotStateMap<Int, PersistentMap<String, PersistentMap<String, Any?>>>
private typealias PersistentPatchesSelection = PersistentMap<Int, PersistentSet<String>>
// Versions of other types, but utilizing observable collection types instead. private fun PatchesSelection.toPersistentPatchesSelection(): PersistentPatchesSelection =
private typealias SnapshotOptions = SnapshotStateMap<Int, SnapshotStateMap<String, SnapshotStateMap<String, Any?>>> mapValues { (_, v) -> v.toPersistentSet() }.toPersistentMap()
private typealias SnapshotExplicitPatchesSelection = SnapshotStateMap<Int, SnapshotStateMap<String, Boolean>>

View file

@ -0,0 +1,128 @@
package app.revanced.manager.ui.viewmodel
import android.content.pm.PackageInfo
import android.os.Parcelable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
import androidx.lifecycle.viewmodel.compose.saveable
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.ui.model.BundleInfo
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchesSelection
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
@OptIn(SavedStateHandleSaveableApi::class)
class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
val bundlesRepo: PatchBundleRepository = get()
private val selectionRepository: PatchSelectionRepository = get()
private val pm: PM = get()
private val savedStateHandle: SavedStateHandle = get()
val prefs: PreferencesManager = get()
var selectedApp by savedStateHandle.saveable {
mutableStateOf(input.app)
}
private set
var selectedAppInfo: PackageInfo? by mutableStateOf(null)
private set
init {
invalidateSelectedAppInfo()
}
var patchOptions: Options by savedStateHandle.saveable {
mutableStateOf(emptyMap())
}
private var selectionState by savedStateHandle.saveable {
if (input.patches != null) {
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
}
val selection: MutableState<SelectionState> = mutableStateOf(SelectionState.Default)
// Get previous selection (if present).
viewModelScope.launch {
val previous = selectionRepository.getSelection(selectedApp.packageName)
if (previous.values.sumOf { it.size } == 0) {
return@launch
}
selection.value = SelectionState.Customized(previous)
}
selection
}
fun setSelectedApp(new: SelectedApp) {
selectedApp = new
invalidateSelectedAppInfo()
}
private fun invalidateSelectedAppInfo() = viewModelScope.launch {
val info = when (val app = selectedApp) {
is SelectedApp.Download -> null
is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) }
is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) }
}
selectedAppInfo = info
}
fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
selectionState.patches(bundles, allowUnsupported)
fun getCustomPatches(
bundles: List<BundleInfo>,
allowUnsupported: Boolean
): PatchesSelection? =
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
fun setCustomPatches(selection: PatchesSelection?) {
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
}
data class Params(
val app: SelectedApp,
val patches: PatchesSelection?,
)
}
private sealed interface SelectionState : Parcelable {
fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean): PatchesSelection
@Parcelize
data class Customized(val patchesSelection: PatchesSelection) : SelectionState {
override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
bundles.toPatchSelection(
allowUnsupported
) { uid, patch ->
patchesSelection[uid]?.contains(patch.name) ?: false
}
}
@Parcelize
data object Default : SelectionState {
override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
bundles.toPatchSelection(allowUnsupported) { _, patch -> patch.include }
}
}

View file

@ -98,7 +98,18 @@ class PM(
null null
} }
fun getPackageInfo(file: File): PackageInfo? = app.packageManager.getPackageArchiveInfo(file.absolutePath, 0) fun getPackageInfo(file: File): PackageInfo? {
val path = file.absolutePath
val pkgInfo = app.packageManager.getPackageArchiveInfo(path, 0) ?: return null
// This is needed in order to load label and icon.
pkgInfo.applicationInfo.apply {
sourceDir = path
publicSourceDir = path
}
return pkgInfo
}
fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString() fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString()

View file

@ -0,0 +1,22 @@
package app.revanced.manager.util.saver
import androidx.compose.runtime.saveable.Saver
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
/**
* Creates a saver that can save nullable versions of types that have custom savers.
*/
fun <Original : Any, Saveable : Any> nullableSaver(baseSaver: Saver<Original, Saveable>): Saver<Original?, Optional<Saveable>> =
Saver(
save = { value ->
with(baseSaver) {
save(value ?: return@Saver Optional.empty())
}?.let {
Optional.of(it)
}
},
restore = {
it.getOrNull()?.let(baseSaver::restore)
}
)

View file

@ -0,0 +1,69 @@
package app.revanced.manager.util.saver
import androidx.compose.runtime.saveable.Saver
import kotlinx.collections.immutable.*
/**
* Create a [Saver] for [PersistentList]s.
*/
fun <T> persistentListSaver() = Saver<PersistentList<T>, List<T>>(
save = {
it.toList()
},
restore = {
it.toPersistentList()
}
)
/**
* Create a [Saver] for [PersistentSet]s.
*/
fun <T> persistentSetSaver() = Saver<PersistentSet<T>, Set<T>>(
save = {
it.toSet()
},
restore = {
it.toPersistentSet()
}
)
/**
* Create a [Saver] for [PersistentMap]s.
*/
fun <K, V> persistentMapSaver() = Saver<PersistentMap<K, V>, Map<K, V>>(
save = {
it.toMap()
},
restore = {
it.toPersistentMap()
}
)
/**
* Create a saver for [PersistentMap]s with a custom [Saver] used for the values.
* Null values will not be saved by this [Saver].
*
* @param valueSaver The [Saver] used for the values of the [Map].
*/
fun <K, Original, Saveable : Any> persistentMapSaver(
valueSaver: Saver<Original, Saveable>
) = Saver<PersistentMap<K, Original>, Map<K, Saveable>>(
save = {
buildMap {
it.forEach { (key, value) ->
with(valueSaver) {
save(value)?.let {
this@buildMap[key] = it
}
}
}
}
},
restore = {
buildMap {
it.forEach { (key, value) ->
this[key] = valueSaver.restore(value) ?: return@forEach
}
}.toPersistentMap()
}
)

View file

@ -25,6 +25,15 @@
<string name="bundle_missing">Missing</string> <string name="bundle_missing">Missing</string>
<string name="bundle_error">Error</string> <string name="bundle_error">Error</string>
<string name="selected_app_meta">%1s • %2d available patches</string>
<string name="patch_item_description">Start patching the application</string>
<string name="patch_selector_item">Patch selection and options</string>
<string name="patch_selector_item_description">%d patches selected</string>
<string name="version_selector_item">Change version</string>
<string name="version_selector_item_description">%s selected</string>
<string name="legacy_import_failed">Could not import legacy settings</string> <string name="legacy_import_failed">Could not import legacy settings</string>