mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2024-11-10 01:01:56 +01:00
feat: selected app info page (#1395)
This commit is contained in:
parent
7ba00cafd9
commit
c3af6acb2c
16 changed files with 779 additions and 309 deletions
|
@ -19,16 +19,17 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
||||
import app.revanced.manager.ui.destination.Destination
|
||||
import app.revanced.manager.ui.screen.AppInfoScreen
|
||||
import app.revanced.manager.ui.screen.InstalledAppInfoScreen
|
||||
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||
import app.revanced.manager.ui.screen.DashboardScreen
|
||||
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.VersionSelectorScreen
|
||||
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.ui.viewmodel.MainViewModel
|
||||
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
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.popUpTo
|
||||
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.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@ExperimentalAnimationApi
|
||||
|
@ -48,7 +49,7 @@ class MainActivity : ComponentActivity() {
|
|||
|
||||
installSplashScreen()
|
||||
|
||||
val vm: MainViewModel = getActivityViewModel()
|
||||
val vm: MainViewModel = getAndroidViewModel()
|
||||
|
||||
setContent {
|
||||
val theme by vm.prefs.theme.getAsState()
|
||||
|
@ -102,7 +103,7 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
|
||||
legacyActivityState = LegacyActivity.LAUNCHED
|
||||
} else if (legacyActivityState == LegacyActivity.FAILED){
|
||||
} else if (legacyActivityState == LegacyActivity.FAILED) {
|
||||
AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
|
||||
}
|
||||
}
|
||||
|
@ -114,15 +115,26 @@ class MainActivity : ComponentActivity() {
|
|||
is Destination.Dashboard -> DashboardScreen(
|
||||
onSettingsClick = { navController.navigate(Destination.Settings) },
|
||||
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 ->
|
||||
navController.navigate(Destination.VersionSelector(packageName, patchesSelection))
|
||||
navController.navigate(
|
||||
Destination.VersionSelector(
|
||||
packageName,
|
||||
patchesSelection
|
||||
)
|
||||
)
|
||||
},
|
||||
onBackClick = { navController.pop() },
|
||||
viewModel = getViewModel { parametersOf(destination.installedApp) }
|
||||
viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
|
||||
)
|
||||
|
||||
is Destination.Settings -> SettingsScreen(
|
||||
|
@ -131,7 +143,13 @@ class MainActivity : ComponentActivity() {
|
|||
|
||||
is Destination.AppSelector -> AppSelectorScreen(
|
||||
onAppClick = { navController.navigate(Destination.VersionSelector(it)) },
|
||||
onStorageClick = { navController.navigate(Destination.PatchesSelector(it)) },
|
||||
onStorageClick = {
|
||||
navController.navigate(
|
||||
Destination.SelectedApplicationInfo(
|
||||
it
|
||||
)
|
||||
)
|
||||
},
|
||||
onBackClick = { navController.pop() }
|
||||
)
|
||||
|
||||
|
@ -139,32 +157,42 @@ class MainActivity : ComponentActivity() {
|
|||
onBackClick = { navController.pop() },
|
||||
onAppClick = { selectedApp ->
|
||||
navController.navigate(
|
||||
Destination.PatchesSelector(
|
||||
Destination.SelectedApplicationInfo(
|
||||
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
|
||||
)
|
||||
)
|
||||
},
|
||||
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(
|
||||
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
|
||||
vm = getViewModel { parametersOf(destination) }
|
||||
vm = getComposeViewModel { parametersOf(destination) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import org.koin.dsl.module
|
|||
val viewModelModule = module {
|
||||
viewModelOf(::MainViewModel)
|
||||
viewModelOf(::DashboardViewModel)
|
||||
viewModelOf(::SelectedAppInfoViewModel)
|
||||
viewModelOf(::PatchesSelectorViewModel)
|
||||
viewModelOf(::SettingsViewModel)
|
||||
viewModelOf(::AdvancedSettingsViewModel)
|
||||
|
@ -19,5 +20,5 @@ val viewModelModule = module {
|
|||
viewModelOf(::ContributorViewModel)
|
||||
viewModelOf(::DownloadsViewModel)
|
||||
viewModelOf(::InstalledAppsViewModel)
|
||||
viewModelOf(::AppInfoViewModel)
|
||||
viewModelOf(::InstalledAppInfoViewModel)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ sealed interface Destination : Parcelable {
|
|||
object Dashboard : Destination
|
||||
|
||||
@Parcelize
|
||||
data class ApplicationInfo(val installedApp: InstalledApp) : Destination
|
||||
data class InstalledApplicationInfo(val installedApp: InstalledApp) : Destination
|
||||
|
||||
@Parcelize
|
||||
object AppSelector : Destination
|
||||
|
@ -26,7 +26,7 @@ sealed interface Destination : Parcelable {
|
|||
data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination
|
||||
|
||||
@Parcelize
|
||||
data class PatchesSelector(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
|
||||
data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
|
||||
|
||||
@Parcelize
|
||||
data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,18 +41,19 @@ import androidx.compose.ui.unit.dp
|
|||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
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.AppTopBar
|
||||
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
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppInfoScreen(
|
||||
fun InstalledAppInfoScreen(
|
||||
onPatchClick: (packageName: String, patchesSelection: PatchesSelection) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: AppInfoViewModel
|
||||
viewModel: InstalledAppInfoViewModel
|
||||
) {
|
||||
SideEffect {
|
||||
viewModel.onBackClick = onBackClick
|
||||
|
@ -80,27 +81,8 @@ fun AppInfoScreen(
|
|||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.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)
|
||||
AppInfo(viewModel.appInfo) {
|
||||
Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
|
||||
|
||||
if (viewModel.installedApp.installType == InstallType.ROOT) {
|
||||
Text(
|
|
@ -15,16 +15,15 @@ import androidx.compose.foundation.pager.HorizontalPager
|
|||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.HelpOutline
|
||||
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.Settings
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FilterChip
|
||||
|
@ -42,6 +41,7 @@ import androidx.compose.material3.TextButton
|
|||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
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.patches.OptionItem
|
||||
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_UNIVERSAL
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
|
||||
|
@ -77,7 +75,7 @@ import org.koin.compose.rememberKoinInject
|
|||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun PatchesSelectorScreen(
|
||||
onPatchClick: (PatchesSelection, Options) -> Unit,
|
||||
onSave: (PatchesSelection?, Options) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
vm: PatchesSelectorViewModel
|
||||
) {
|
||||
|
@ -93,10 +91,10 @@ fun PatchesSelectorScreen(
|
|||
mutableStateOf(null)
|
||||
}
|
||||
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
|
||||
var showPatchButton by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(Unit) {
|
||||
showPatchButton = vm.isSelectionNotEmpty()
|
||||
val showPatchButton by remember {
|
||||
derivedStateOf { vm.selectionIsValid(bundles) }
|
||||
}
|
||||
|
||||
if (showBottomSheet) {
|
||||
ModalBottomSheet(
|
||||
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())
|
||||
UnsupportedDialog(
|
||||
appVersion = vm.input.selectedApp.version,
|
||||
appVersion = vm.appVersion,
|
||||
supportedVersions = vm.compatibleVersions,
|
||||
onDismissRequest = vm::dismissDialogs
|
||||
)
|
||||
|
@ -194,8 +165,6 @@ fun PatchesSelectorScreen(
|
|||
)
|
||||
}
|
||||
|
||||
val allowExperimental by vm.allowExperimental.getAsState()
|
||||
|
||||
fun LazyListScope.patchList(
|
||||
uid: Int,
|
||||
patches: List<PatchInfo>,
|
||||
|
@ -227,17 +196,10 @@ fun PatchesSelectorScreen(
|
|||
if (vm.selectionWarningEnabled) {
|
||||
vm.pendingSelectionAction = {
|
||||
vm.togglePatch(uid, patch)
|
||||
vm.viewModelScope.launch {
|
||||
showPatchButton = vm.isSelectionNotEmpty()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vm.togglePatch(uid, patch)
|
||||
vm.viewModelScope.launch {
|
||||
showPatchButton = vm.isSelectionNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
supported = supported
|
||||
)
|
||||
|
@ -292,7 +254,7 @@ fun PatchesSelectorScreen(
|
|||
)
|
||||
}
|
||||
|
||||
if (!allowExperimental) return@LazyColumn
|
||||
if (!vm.allowExperimental) return@LazyColumn
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.unsupported.searched(),
|
||||
|
@ -332,22 +294,19 @@ fun PatchesSelectorScreen(
|
|||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if(showPatchButton) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
Text(stringResource(R.string.patch))
|
||||
},
|
||||
icon = { Icon(Icons.Default.Build, null) },
|
||||
onClick = {
|
||||
// TODO: only allow this if all required options have been set.
|
||||
composableScope.launch {
|
||||
val selection = vm.getSelection()
|
||||
vm.saveSelection(selection).join()
|
||||
onPatchClick(selection, vm.getOptions())
|
||||
}
|
||||
if (!showPatchButton) return@Scaffold
|
||||
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.save)) },
|
||||
icon = { Icon(Icons.Outlined.Save, null) },
|
||||
onClick = {
|
||||
// TODO: only allow this if all required options have been set.
|
||||
composableScope.launch {
|
||||
vm.saveSelection()
|
||||
onSave(vm.getCustomSelection(), vm.getOptions())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
|
@ -407,7 +366,7 @@ fun PatchesSelectorScreen(
|
|||
uid = bundle.uid,
|
||||
patches = bundle.unsupported,
|
||||
filterFlag = SHOW_UNSUPPORTED,
|
||||
supported = allowExperimental
|
||||
supported = vm.allowExperimental
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.unsupported_patches),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
|
@ -30,7 +30,7 @@ import kotlinx.coroutines.withContext
|
|||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class AppInfoViewModel(
|
||||
class InstalledAppInfoViewModel(
|
||||
val installedApp: InstalledApp
|
||||
) : ViewModel(), KoinComponent {
|
||||
private val app: Application by inject()
|
|
@ -2,8 +2,8 @@ package app.revanced.manager.ui.viewmodel
|
|||
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
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.PatchBundleRepository
|
||||
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.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.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import kotlinx.collections.immutable.*
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Optional
|
||||
|
||||
@Stable
|
||||
@OptIn(SavedStateHandleSaveableApi::class)
|
||||
class PatchesSelectorViewModel(
|
||||
val input: Destination.PatchesSelector
|
||||
) : ViewModel(), KoinComponent {
|
||||
class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||
private val app: Application = get()
|
||||
private val selectionRepository: PatchSelectionRepository = get()
|
||||
private val savedStateHandle: SavedStateHandle = 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)
|
||||
|
||||
// TODO: this should be hoisted to the parent screen
|
||||
var selectionWarningEnabled by mutableStateOf(true)
|
||||
private set
|
||||
|
||||
val allowExperimental = get<PreferencesManager>().allowExperimental
|
||||
val bundlesFlow = get<PatchBundleRepository>().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(
|
||||
input.selectedApp.packageName,
|
||||
input.selectedApp.version
|
||||
) -> supported
|
||||
|
||||
else -> unsupported
|
||||
}
|
||||
|
||||
targetList.add(it)
|
||||
}
|
||||
|
||||
BundleInfo(source.name, source.uid, bundle.patches, supported, unsupported, universal)
|
||||
}
|
||||
}
|
||||
val allowExperimental = get<PreferencesManager>().allowExperimental.getBlocking()
|
||||
val bundlesFlow =
|
||||
get<PatchBundleRepository>().bundleInfoFlow(packageName, input.app.version)
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
|
@ -88,63 +68,28 @@ class PatchesSelectorViewModel(
|
|||
return@launch
|
||||
}
|
||||
|
||||
val experimental = allowExperimental.get()
|
||||
fun BundleInfo.hasDefaultPatches(): Boolean {
|
||||
return if (experimental) {
|
||||
all.asSequence()
|
||||
} else {
|
||||
sequence {
|
||||
yieldAll(supported)
|
||||
yieldAll(universal)
|
||||
}
|
||||
}.any { it.include }
|
||||
}
|
||||
fun BundleInfo.hasDefaultPatches() = patchSequence(allowExperimental).any { it.include }
|
||||
|
||||
// Don't show the warning if there are no default patches.
|
||||
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 customPatchesSelection: PersistentPatchesSelection? by savedStateHandle.saveable(
|
||||
key = "selection",
|
||||
stateSaver = patchesSaver,
|
||||
) {
|
||||
mutableStateOf(input.currentSelection?.toPersistentPatchesSelection())
|
||||
}
|
||||
|
||||
private val explicitPatchesSelection: SnapshotExplicitPatchesSelection by savedStateHandle.saveable(
|
||||
saver = explicitPatchesSelectionSaver,
|
||||
init = ::mutableStateMapOf
|
||||
)
|
||||
|
||||
private val patchOptions: SnapshotOptions by savedStateHandle.saveable(
|
||||
private val patchOptions: PersistentOptions by savedStateHandle.saveable(
|
||||
saver = optionsSaver,
|
||||
init = ::mutableStateMapOf
|
||||
)
|
||||
|
||||
private val selectors by derivedStateOf<Array<Selector>> {
|
||||
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
|
||||
})
|
||||
}
|
||||
)
|
||||
) {
|
||||
// Convert Options to PersistentOptions
|
||||
input.options.mapValuesTo(mutableStateMapOf()) { (_, allPatches) ->
|
||||
allPatches.mapValues { (_, options) -> options.toPersistentMap() }.toPersistentMap()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -154,52 +99,39 @@ class PatchesSelectorViewModel(
|
|||
|
||||
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 suspend fun loadPreviousSelection() {
|
||||
val selection = (input.patchesSelection ?: selectionRepository.getSelection(
|
||||
packageName
|
||||
)).mapValues { (_, value) -> value.toSet() }
|
||||
private suspend fun generateDefaultSelection(): PersistentPatchesSelection {
|
||||
val bundles = bundlesFlow.first()
|
||||
val generatedSelection =
|
||||
bundles.toPatchSelection(allowExperimental) { _, patch -> patch.include }
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
previousPatchesSelection.putAll(selection)
|
||||
return generatedSelection.toPersistentPatchesSelection()
|
||||
}
|
||||
|
||||
fun selectionIsValid(bundles: List<BundleInfo>) = bundles.any { bundle ->
|
||||
bundle.patchSequence(allowExperimental).any { patch ->
|
||||
isSelected(bundle.uid, patch)
|
||||
}
|
||||
}
|
||||
|
||||
fun switchBaseSelectionMode() = viewModelScope.launch {
|
||||
baseSelectionMode = if (baseSelectionMode == BaseSelectionMode.DEFAULT) {
|
||||
BaseSelectionMode.PREVIOUS
|
||||
} 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 isSelected(bundle: Int, patch: PatchInfo) = customPatchesSelection?.let { selection ->
|
||||
selection[bundle]?.contains(patch.name) ?: false
|
||||
} ?: patch.include
|
||||
|
||||
fun togglePatch(bundle: Int, patch: PatchInfo) = viewModelScope.launch {
|
||||
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) {
|
||||
|
@ -221,46 +153,39 @@ class PatchesSelectorViewModel(
|
|||
|
||||
fun reset() {
|
||||
patchOptions.clear()
|
||||
baseSelectionMode = BaseSelectionMode.DEFAULT
|
||||
explicitPatchesSelection.clear()
|
||||
customPatchesSelection = null
|
||||
hasModifiedSelection = false
|
||||
app.toast(app.getString(R.string.patch_selection_reset_toast))
|
||||
}
|
||||
|
||||
suspend fun getSelection(): PatchesSelection {
|
||||
val bundles = bundlesFlow.first()
|
||||
val removeUnsupported = !allowExperimental.get()
|
||||
fun getCustomSelection(): PatchesSelection? {
|
||||
// Convert persistent collections to standard hash collections because persistent collections are not parcelable.
|
||||
|
||||
return bundles.associate { bundle ->
|
||||
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
|
||||
}
|
||||
return customPatchesSelection?.mapValues { (_, v) -> v.toSet() }
|
||||
}
|
||||
|
||||
suspend fun saveSelection(selection: PatchesSelection) =
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
when {
|
||||
hasModifiedSelection -> selectionRepository.updateSelection(packageName, selection)
|
||||
baseSelectionMode == BaseSelectionMode.DEFAULT -> selectionRepository.clearSelection(
|
||||
packageName
|
||||
)
|
||||
fun getOptions(): Options {
|
||||
// Convert the collection for the same reasons as in getCustomSelection()
|
||||
|
||||
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 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) {
|
||||
|
@ -274,7 +199,7 @@ class PatchesSelectorViewModel(
|
|||
|
||||
fun openUnsupportedDialog(unsupportedPatches: List<PatchInfo>) {
|
||||
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_UNSUPPORTED = 4 // 2^2
|
||||
|
||||
private fun <K, K2, V> SnapshotStateMap<K, SnapshotStateMap<K2, V>>.getOrCreate(key: K) =
|
||||
getOrPut(key, ::mutableStateMapOf)
|
||||
|
||||
private val optionsSaver: Saver<SnapshotOptions, Options> = snapshotStateMapSaver(
|
||||
private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver(
|
||||
// Patch name -> Options
|
||||
valueSaver = snapshotStateMapSaver(
|
||||
valueSaver = persistentMapSaver(
|
||||
// Option key -> Option value
|
||||
valueSaver = snapshotStateMapSaver()
|
||||
valueSaver = persistentMapSaver()
|
||||
)
|
||||
)
|
||||
|
||||
private val explicitPatchesSelectionSaver: Saver<SnapshotExplicitPatchesSelection, ExplicitPatchesSelection> =
|
||||
snapshotStateMapSaver(valueSaver = snapshotStateMapSaver())
|
||||
private val patchesSaver: Saver<PersistentPatchesSelection?, Optional<PatchesSelection>> =
|
||||
nullableSaver(persistentMapSaver(valueSaver = persistentSetSaver()))
|
||||
}
|
||||
|
||||
/**
|
||||
* An enum for controlling the behavior of the selector.
|
||||
*/
|
||||
enum class BaseSelectionMode {
|
||||
/**
|
||||
* 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>
|
||||
data class Params(
|
||||
val app: SelectedApp,
|
||||
val currentSelection: PatchesSelection?,
|
||||
val options: Options,
|
||||
)
|
||||
}
|
||||
|
||||
private typealias Selector = (Int, PatchInfo) -> Boolean?
|
||||
private typealias ExplicitPatchesSelection = Map<Int, Map<String, Boolean>>
|
||||
// Versions of other types, but utilizing persistent/observable collection types.
|
||||
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 typealias SnapshotOptions = SnapshotStateMap<Int, SnapshotStateMap<String, SnapshotStateMap<String, Any?>>>
|
||||
private typealias SnapshotExplicitPatchesSelection = SnapshotStateMap<Int, SnapshotStateMap<String, Boolean>>
|
||||
private fun PatchesSelection.toPersistentPatchesSelection(): PersistentPatchesSelection =
|
||||
mapValues { (_, v) -> v.toPersistentSet() }.toPersistentMap()
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -98,7 +98,18 @@ class PM(
|
|||
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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
|
@ -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()
|
||||
}
|
||||
)
|
|
@ -25,6 +25,15 @@
|
|||
|
||||
<string name="bundle_missing">Missing</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>
|
||||
|
||||
|
|
Loading…
Reference in a new issue