feat: add required options screen

This commit is contained in:
Ax333l 2025-01-03 22:20:01 +01:00
parent 9db3bd5b3f
commit 7e4f5653d3
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
10 changed files with 337 additions and 51 deletions

View File

@ -12,6 +12,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
@ -29,6 +30,7 @@ import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel import app.revanced.manager.ui.viewmodel.MainViewModel
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.EventEffect import app.revanced.manager.util.EventEffect
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.androidx.compose.navigation.koinNavViewModel import org.koin.androidx.compose.navigation.koinNavViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
@ -139,14 +141,20 @@ private fun ReVancedManager(vm: MainViewModel) {
val parentBackStackEntry = navController.navGraphEntry(it) val parentBackStackEntry = navController.navGraphEntry(it)
val data = val data =
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>() parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
val viewModel =
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
parametersOf(data)
}
SelectedAppInfoScreen( SelectedAppInfoScreen(
onBackClick = navController::popBackStack, onBackClick = navController::popBackStack,
onPatchClick = { app, patches, options -> onPatchClick = {
navController.navigateComplex( it.lifecycleScope.launch {
Patcher, navController.navigateComplex(
Patcher.ViewModelParams(app, patches, options) Patcher,
) viewModel.getPatcherParams()
)
}
}, },
onPatchSelectorClick = { app, patches, options -> onPatchSelectorClick = { app, patches, options ->
navController.navigateComplex( navController.navigateComplex(
@ -158,9 +166,17 @@ private fun ReVancedManager(vm: MainViewModel) {
) )
) )
}, },
vm = koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) { onRequiredOptions = { app, patches, options ->
parametersOf(data) navController.navigateComplex(
} SelectedApplicationInfo.RequiredOptions,
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
app,
patches,
options
)
)
},
vm = viewModel
) )
} }
@ -180,6 +196,28 @@ private fun ReVancedManager(vm: MainViewModel) {
vm = koinViewModel { parametersOf(data) } vm = koinViewModel { parametersOf(data) }
) )
} }
composable<SelectedApplicationInfo.RequiredOptions> {
val data =
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
viewModelStoreOwner = navController.navGraphEntry(it)
)
RequiredOptionsScreen(
onBackClick = navController::popBackStack,
onContinue = { patches, options ->
selectedAppInfoVm.updateConfiguration(patches, options)
it.lifecycleScope.launch {
navController.navigateComplex(
Patcher,
selectedAppInfoVm.getPatcherParams()
)
}
},
vm = koinViewModel { parametersOf(data) }
)
}
} }
navigation<Settings>(startDestination = Settings.Main) { navigation<Settings>(startDestination = Settings.Main) {

View File

@ -8,6 +8,7 @@ import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -141,13 +142,19 @@ private inline fun <T : Any> WithOptionEditor(
} }
@Composable @Composable
fun <T : Any> OptionItem(option: Option<T>, value: T?, setValue: (T?) -> Unit) { fun <T : Any> OptionItem(
option: Option<T>,
value: T?,
setValue: (T?) -> Unit,
) {
val editor = remember(option.type, option.presets) { val editor = remember(option.type, option.presets) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val baseOptionEditor = val baseOptionEditor =
optionEditors.getOrDefault(option.type, UnknownTypeEditor) as OptionEditor<T> optionEditors.getOrDefault(option.type, UnknownTypeEditor) as OptionEditor<T>
if (option.type != typeOf<Boolean>() && option.presets != null) PresetOptionEditor(baseOptionEditor) if (option.type != typeOf<Boolean>() && option.presets != null) PresetOptionEditor(
baseOptionEditor
)
else baseOptionEditor else baseOptionEditor
} }
@ -155,7 +162,15 @@ fun <T : Any> OptionItem(option: Option<T>, value: T?, setValue: (T?) -> Unit) {
ListItem( ListItem(
modifier = Modifier.clickable(onClick = ::clickAction), modifier = Modifier.clickable(onClick = ::clickAction),
headlineContent = { Text(option.title) }, headlineContent = { Text(option.title) },
supportingContent = { Text(option.description) }, supportingContent = {
Column {
Text(option.description)
if (option.required && value == null) Text(
stringResource(R.string.option_required),
color = MaterialTheme.colorScheme.error
)
}
},
trailingContent = { ListItemTrailingContent() } trailingContent = { ListItemTrailingContent() }
) )
} }

View File

@ -34,20 +34,23 @@ data class BundleInfo(
} }
companion object Extensions { companion object Extensions {
inline fun Iterable<BundleInfo>.toPatchSelection(allowUnsupported: Boolean, condition: (Int, PatchInfo) -> Boolean): PatchSelection = this.associate { bundle -> inline fun Iterable<BundleInfo>.toPatchSelection(
val patches = allowUnsupported: Boolean,
bundle.patchSequence(allowUnsupported) condition: (Int, PatchInfo) -> Boolean
.mapNotNullTo(mutableSetOf()) { patch -> ): PatchSelection = this.associate { bundle ->
patch.name.takeIf { val patches =
condition( bundle.patchSequence(allowUnsupported)
bundle.uid, .mapNotNullTo(mutableSetOf()) { patch ->
patch patch.name.takeIf {
) condition(
} bundle.uid,
patch
)
} }
}
bundle.uid to patches bundle.uid to patches
} }
fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String?) = fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String?) =
sources.flatMapLatestAndCombine( sources.flatMapLatestAndCombine(
@ -78,6 +81,28 @@ data class BundleInfo(
BundleInfo(source.getName(), source.uid, supported, unsupported, universal) BundleInfo(source.getName(), source.uid, supported, unsupported, universal)
} }
} }
/**
* Algorithm for determining whether all required options have been set.
*/
inline fun Iterable<BundleInfo>.requiredOptionsSet(
crossinline isSelected: (BundleInfo, PatchInfo) -> Boolean,
crossinline optionsForPatch: (BundleInfo, PatchInfo) -> Map<String, Any?>?
) = all bundle@{ bundle ->
bundle
.all
.filter { isSelected(bundle, it) }
.all patch@{
if (it.options.isNullOrEmpty()) return@patch true
val opts by lazy { optionsForPatch(bundle, it).orEmpty() }
it.options.all option@{ option ->
if (!option.required || option.default != null) return@option true
option.key in opts
}
}
}
} }
} }

View File

@ -42,6 +42,9 @@ data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.V
val options: @RawValue Options, val options: @RawValue Options,
) : Parcelable ) : Parcelable
} }
@Serializable
data object RequiredOptions : ComplexParameter<PatchesSelector.ViewModelParams>
} }
@Serializable @Serializable

View File

@ -67,7 +67,7 @@ fun PatchesSelectorScreen(
mutableStateOf(null) mutableStateOf(null)
} }
var showBottomSheet by rememberSaveable { mutableStateOf(false) } var showBottomSheet by rememberSaveable { mutableStateOf(false) }
val showPatchButton by remember { val showSaveButton by remember {
derivedStateOf { vm.selectionIsValid(bundles) } derivedStateOf { vm.selectionIsValid(bundles) }
} }
@ -298,7 +298,7 @@ fun PatchesSelectorScreen(
) )
}, },
floatingActionButton = { floatingActionButton = {
if (!showPatchButton) return@Scaffold if (!showSaveButton) return@Scaffold
HapticExtendedFloatingActionButton( HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.save)) }, text = { Text(stringResource(R.string.save)) },
@ -311,7 +311,6 @@ fun PatchesSelectorScreen(
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
?: true, ?: true,
onClick = { onClick = {
// TODO: only allow this if all required options have been set.
onSave(vm.getCustomSelection(), vm.getOptions()) onSave(vm.getCustomSelection(), vm.getOptions())
} }
) )
@ -464,7 +463,7 @@ private fun PatchItem(
) )
@Composable @Composable
private fun ListHeader( fun ListHeader(
title: String, title: String,
onHelpClick: (() -> Unit)? = null onHelpClick: (() -> Unit)? = null
) { ) {

View File

@ -0,0 +1,158 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AutoFixHigh
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.patcher.patch.Option
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticTab
import app.revanced.manager.ui.component.patches.OptionItem
import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.isScrollingUp
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RequiredOptionsScreen(
onContinue: (PatchSelection?, Options) -> Unit,
onBackClick: () -> Unit,
vm: PatchesSelectorViewModel
) {
val list by vm.requiredOptsPatches.collectAsStateWithLifecycle(emptyList())
val pagerState = rememberPagerState(
initialPage = 0,
initialPageOffsetFraction = 0f
) {
list.size
}
val patchLazyListStates = remember(list) { List(list.size, ::LazyListState) }
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(emptyList())
val showContinueButton by remember {
derivedStateOf {
bundles.requiredOptionsSet(
isSelected = { bundle, patch -> vm.isSelected(bundle.uid, patch) },
optionsForPatch = { bundle, patch -> vm.getOptions(bundle.uid, patch) }
)
}
}
val composableScope = rememberCoroutineScope()
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.required_options_screen),
onBackClick = onBackClick
)
},
floatingActionButton = {
if (!showContinueButton) return@Scaffold
HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.patch)) },
icon = {
Icon(
Icons.Default.AutoFixHigh,
stringResource(R.string.patch)
)
},
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
?: true,
onClick = {
onContinue(vm.getCustomSelection(), vm.getOptions())
}
)
}
) { paddingValues ->
Column(
Modifier
.fillMaxSize()
.padding(paddingValues)
) {
if (list.isEmpty()) return@Column
else if (list.size > 1) ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) {
list.forEachIndexed { index, (bundle, _) ->
HapticTab(
selected = pagerState.currentPage == index,
onClick = {
composableScope.launch {
pagerState.animateScrollToPage(
index
)
}
},
text = { Text(bundle.name) },
selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
HorizontalPager(
state = pagerState,
userScrollEnabled = true,
pageContent = { index ->
// Avoid crashing if the lists have not been fully initialized yet.
if (index > list.lastIndex || list.size != patchLazyListStates.size) return@HorizontalPager
val (bundle, patches) = list[index]
LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize(),
state = patchLazyListStates[index]
) {
items(patches, key = { it.name }) {
ListHeader(it.name)
val values = vm.getOptions(bundle.uid, it)
it.options?.forEach { option ->
val key = option.key
val value =
if (values == null || key !in values) option.default else values[key]
@Suppress("UNCHECKED_CAST")
OptionItem(
option = option as Option<Any>,
value = value,
setValue = { new ->
vm.setOption(bundle.uid, it, key, new)
}
)
}
}
}
}
)
}
}
}

View File

@ -14,9 +14,9 @@ import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.filled.AutoFixHigh import androidx.compose.material.icons.filled.AutoFixHigh
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -32,7 +32,6 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.EventEffect import app.revanced.manager.util.EventEffect
@ -41,12 +40,14 @@ import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.enabled import app.revanced.manager.util.enabled
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.transparentListItemColors import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SelectedAppInfoScreen( fun SelectedAppInfoScreen(
onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit, onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit,
onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit, onRequiredOptions: (SelectedApp, PatchSelection?, Options) -> Unit,
onPatchClick: () -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: SelectedAppInfoViewModel vm: SelectedAppInfoViewModel
) { ) {
@ -54,20 +55,14 @@ fun SelectedAppInfoScreen(
val packageName = vm.selectedApp.packageName val packageName = vm.selectedApp.packageName
val version = vm.selectedApp.version val version = vm.selectedApp.version
val bundles by remember(packageName, version) { val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
vm.bundlesRepo.bundleInfoFlow(packageName, version)
}.collectAsStateWithLifecycle(initialValue = emptyList())
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState() val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
val patches by remember { val patches = remember(bundles, allowIncompatiblePatches) {
derivedStateOf { vm.getPatches(bundles, allowIncompatiblePatches)
vm.getPatches(bundles, allowIncompatiblePatches)
}
} }
val selectedPatchCount by remember { val selectedPatchCount = remember(patches) {
derivedStateOf { patches.values.sumOf { it.size }
patches.values.sumOf { it.size }
}
} }
val launcher = rememberLauncherForActivityResult( val launcher = rememberLauncherForActivityResult(
@ -77,6 +72,7 @@ fun SelectedAppInfoScreen(
EventEffect(flow = vm.launchActivityFlow) { intent -> EventEffect(flow = vm.launchActivityFlow) { intent ->
launcher.launch(intent) launcher.launch(intent)
} }
val composableScope = rememberCoroutineScope()
val error by vm.errorFlow.collectAsStateWithLifecycle(null) val error by vm.errorFlow.collectAsStateWithLifecycle(null)
Scaffold( Scaffold(
@ -103,11 +99,19 @@ fun SelectedAppInfoScreen(
return@patchClick return@patchClick
} }
onPatchClick(
vm.selectedApp, composableScope.launch {
patches, if (!vm.hasSetRequiredOptions(patches)) {
vm.getOptionsFiltered(bundles) onRequiredOptions(
) vm.selectedApp,
vm.getCustomPatches(bundles, allowIncompatiblePatches),
vm.options
)
return@launch
}
onPatchClick()
}
} }
) )
} }

View File

@ -22,7 +22,6 @@ import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.model.BundleInfo import app.revanced.manager.ui.model.BundleInfo
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
@ -37,11 +36,14 @@ import kotlinx.coroutines.launch
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.collections.immutable.*
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@Stable
@OptIn(SavedStateHandleSaveableApi::class) @OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) : ViewModel(), KoinComponent { class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) :
ViewModel(), KoinComponent {
private val app: Application = get() private val app: Application = get()
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
private val prefs: PreferencesManager = get() private val prefs: PreferencesManager = get()
@ -114,6 +116,22 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
selection.values.sumOf { it.size } selection.values.sumOf { it.size }
} }
// This is for the required options screen.
private val requiredOptsPatchesDeferred = viewModelScope.async(start = CoroutineStart.LAZY) {
bundlesFlow.first().map { bundle ->
bundle to bundle.all.filter { patch ->
val opts by lazy {
getOptions(bundle.uid, patch).orEmpty()
}
isSelected(
bundle.uid,
patch
) && patch.options?.any { it.required && it.default == null && it.key !in opts } ?: false
}.toList()
}.filter { (_, patches) -> patches.isNotEmpty() }
}
val requiredOptsPatches = flow { emit(requiredOptsPatchesDeferred.await()) }
fun selectionIsValid(bundles: List<BundleInfo>) = bundles.any { bundle -> fun selectionIsValid(bundles: List<BundleInfo>) = bundles.any { bundle ->
bundle.patchSequence(allowIncompatiblePatches).any { patch -> bundle.patchSequence(allowIncompatiblePatches).any { patch ->
isSelected(bundle.uid, patch) isSelected(bundle.uid, patch)

View File

@ -9,6 +9,7 @@ import android.util.Log
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
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.setValue import androidx.compose.runtime.setValue
@ -29,13 +30,16 @@ import app.revanced.manager.domain.repository.PatchOptionsRepository
import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.ParceledDownloaderData import app.revanced.manager.network.downloader.ParceledDownloaderData
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.plugin.downloader.GetScope import app.revanced.manager.plugin.downloader.GetScope
import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.ui.model.BundleInfo import app.revanced.manager.ui.model.BundleInfo
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
@ -63,7 +67,6 @@ class SelectedAppInfoViewModel(
input: SelectedApplicationInfo.ViewModelParams input: SelectedApplicationInfo.ViewModelParams
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent {
private val app: Application = get() private val app: Application = get()
val bundlesRepo: PatchBundleRepository = get()
private val bundleRepository: PatchBundleRepository = get() private val bundleRepository: PatchBundleRepository = get()
private val selectionRepository: PatchSelectionRepository = get() private val selectionRepository: PatchSelectionRepository = get()
private val optionsRepository: PatchOptionsRepository = get() private val optionsRepository: PatchOptionsRepository = get()
@ -174,6 +177,10 @@ class SelectedAppInfoViewModel(
} }
} }
val bundleInfoFlow by derivedStateOf {
bundleRepository.bundleInfoFlow(packageName, selectedApp.version)
}
fun showSourceSelector() { fun showSourceSelector() {
dismissSourceSelector() dismissSourceSelector()
showSourceSelector = true showSourceSelector = true
@ -260,6 +267,23 @@ class SelectedAppInfoViewModel(
selectedAppInfo = info selectedAppInfo = info
} }
suspend fun hasSetRequiredOptions(patchSelection: PatchSelection) = bundleInfoFlow
.first()
.requiredOptionsSet(
isSelected = { bundle, patch -> patch.name in patchSelection[bundle.uid]!! },
optionsForPatch = { bundle, patch -> options[bundle.uid]?.get(patch.name) },
)
suspend fun getPatcherParams(): Patcher.ViewModelParams {
val allowUnsupported = prefs.disablePatchVersionCompatCheck.get()
val bundles = bundleInfoFlow.first()
return Patcher.ViewModelParams(
selectedApp,
getPatches(bundles, allowUnsupported),
getOptionsFiltered(bundles)
)
}
fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles) fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles)
fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) = fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
@ -272,7 +296,7 @@ class SelectedAppInfoViewModel(
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported) (selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch { fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch {
val bundles = bundlesRepo.bundleInfoFlow(packageName, selectedApp.version).first() val bundles = bundleInfoFlow.first()
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default

View File

@ -365,6 +365,8 @@
<string name="invalid_date">Invalid date</string> <string name="invalid_date">Invalid date</string>
<string name="disable_battery_optimization">Disable battery optimization</string> <string name="disable_battery_optimization">Disable battery optimization</string>
<string name="input_dialog_value_invalid">Invalid value</string> <string name="input_dialog_value_invalid">Invalid value</string>
<string name="option_required">This option is required</string>
<string name="required_options_screen">Required options</string>
<string name="failed_to_check_updates">Failed to check for updates: %s</string> <string name="failed_to_check_updates">Failed to check for updates: %s</string>
<string name="no_update_available">No update available</string> <string name="no_update_available">No update available</string>