From 7e4f5653d3d528e08f1a91272d11e779084f5f78 Mon Sep 17 00:00:00 2001 From: Ax333l Date: Fri, 3 Jan 2025 22:20:01 +0100 Subject: [PATCH] feat: add required options screen --- .../java/app/revanced/manager/MainActivity.kt | 54 +++++- .../ui/component/patches/OptionFields.kt | 21 ++- .../revanced/manager/ui/model/BundleInfo.kt | 49 ++++-- .../manager/ui/model/navigation/Nav.kt | 3 + .../ui/screen/PatchesSelectorScreen.kt | 7 +- .../ui/screen/RequiredOptionsScreen.kt | 158 ++++++++++++++++++ .../ui/screen/SelectedAppInfoScreen.kt | 42 ++--- .../ui/viewmodel/PatchesSelectorViewModel.kt | 24 ++- .../ui/viewmodel/SelectedAppInfoViewModel.kt | 28 +++- app/src/main/res/values/strings.xml | 2 + 10 files changed, 337 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 6139348135..72f046dc15 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController 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.SelectedAppInfoViewModel import app.revanced.manager.util.EventEffect +import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.navigation.koinNavViewModel import org.koin.core.parameter.parametersOf @@ -139,14 +141,20 @@ private fun ReVancedManager(vm: MainViewModel) { val parentBackStackEntry = navController.navGraphEntry(it) val data = parentBackStackEntry.getComplexArg() + val viewModel = + koinNavViewModel(viewModelStoreOwner = parentBackStackEntry) { + parametersOf(data) + } SelectedAppInfoScreen( onBackClick = navController::popBackStack, - onPatchClick = { app, patches, options -> - navController.navigateComplex( - Patcher, - Patcher.ViewModelParams(app, patches, options) - ) + onPatchClick = { + it.lifecycleScope.launch { + navController.navigateComplex( + Patcher, + viewModel.getPatcherParams() + ) + } }, onPatchSelectorClick = { app, patches, options -> navController.navigateComplex( @@ -158,9 +166,17 @@ private fun ReVancedManager(vm: MainViewModel) { ) ) }, - vm = koinNavViewModel(viewModelStoreOwner = parentBackStackEntry) { - parametersOf(data) - } + onRequiredOptions = { app, patches, options -> + 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) } ) } + + composable { + val data = + it.getComplexArg() + val selectedAppInfoVm = koinNavViewModel( + 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(startDestination = Settings.Main) { diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt index 3c0504bceb..06438176b8 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth @@ -141,13 +142,19 @@ private inline fun WithOptionEditor( } @Composable -fun OptionItem(option: Option, value: T?, setValue: (T?) -> Unit) { +fun OptionItem( + option: Option, + value: T?, + setValue: (T?) -> Unit, +) { val editor = remember(option.type, option.presets) { @Suppress("UNCHECKED_CAST") val baseOptionEditor = optionEditors.getOrDefault(option.type, UnknownTypeEditor) as OptionEditor - if (option.type != typeOf() && option.presets != null) PresetOptionEditor(baseOptionEditor) + if (option.type != typeOf() && option.presets != null) PresetOptionEditor( + baseOptionEditor + ) else baseOptionEditor } @@ -155,7 +162,15 @@ fun OptionItem(option: Option, value: T?, setValue: (T?) -> Unit) { ListItem( modifier = Modifier.clickable(onClick = ::clickAction), 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() } ) } diff --git a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt index e2bd8b1eb2..9dd9d1b053 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt @@ -34,20 +34,23 @@ data class BundleInfo( } companion object Extensions { - inline fun Iterable.toPatchSelection(allowUnsupported: Boolean, condition: (Int, PatchInfo) -> Boolean): PatchSelection = this.associate { bundle -> - val patches = - bundle.patchSequence(allowUnsupported) - .mapNotNullTo(mutableSetOf()) { patch -> - patch.name.takeIf { - condition( - bundle.uid, - patch - ) - } + inline fun Iterable.toPatchSelection( + allowUnsupported: Boolean, + condition: (Int, PatchInfo) -> Boolean + ): PatchSelection = this.associate { bundle -> + val patches = + bundle.patchSequence(allowUnsupported) + .mapNotNullTo(mutableSetOf()) { patch -> + patch.name.takeIf { + condition( + bundle.uid, + patch + ) } + } - bundle.uid to patches - } + bundle.uid to patches + } fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String?) = sources.flatMapLatestAndCombine( @@ -78,6 +81,28 @@ data class BundleInfo( BundleInfo(source.getName(), source.uid, supported, unsupported, universal) } } + + /** + * Algorithm for determining whether all required options have been set. + */ + inline fun Iterable.requiredOptionsSet( + crossinline isSelected: (BundleInfo, PatchInfo) -> Boolean, + crossinline optionsForPatch: (BundleInfo, PatchInfo) -> Map? + ) = 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 + } + } + } } } diff --git a/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt b/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt index ad235a3abe..c4063ebb26 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt @@ -42,6 +42,9 @@ data object SelectedApplicationInfo : ComplexParameter } @Serializable diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt index e38f320b6c..69e0d45eb7 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt @@ -67,7 +67,7 @@ fun PatchesSelectorScreen( mutableStateOf(null) } var showBottomSheet by rememberSaveable { mutableStateOf(false) } - val showPatchButton by remember { + val showSaveButton by remember { derivedStateOf { vm.selectionIsValid(bundles) } } @@ -298,7 +298,7 @@ fun PatchesSelectorScreen( ) }, floatingActionButton = { - if (!showPatchButton) return@Scaffold + if (!showSaveButton) return@Scaffold HapticExtendedFloatingActionButton( text = { Text(stringResource(R.string.save)) }, @@ -311,7 +311,6 @@ fun PatchesSelectorScreen( expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp ?: true, onClick = { - // TODO: only allow this if all required options have been set. onSave(vm.getCustomSelection(), vm.getOptions()) } ) @@ -464,7 +463,7 @@ private fun PatchItem( ) @Composable -private fun ListHeader( +fun ListHeader( title: String, onHelpClick: (() -> Unit)? = null ) { diff --git a/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt new file mode 100644 index 0000000000..c441f1ac39 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt @@ -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, + value = value, + setValue = { new -> + vm.setOption(bundle.uid, it, key, new) + } + ) + } + } + } + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt index 00441daef1..990b654872 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt @@ -14,9 +14,9 @@ import androidx.compose.material.icons.automirrored.outlined.ArrowRight import androidx.compose.material.icons.filled.AutoFixHigh import androidx.compose.material3.* 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.platform.LocalContext 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.LoadingIndicator 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.viewmodel.SelectedAppInfoViewModel 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.toast import app.revanced.manager.util.transparentListItemColors +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun SelectedAppInfoScreen( onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit, - onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit, + onRequiredOptions: (SelectedApp, PatchSelection?, Options) -> Unit, + onPatchClick: () -> Unit, onBackClick: () -> Unit, vm: SelectedAppInfoViewModel ) { @@ -54,20 +55,14 @@ fun SelectedAppInfoScreen( val packageName = vm.selectedApp.packageName val version = vm.selectedApp.version - val bundles by remember(packageName, version) { - vm.bundlesRepo.bundleInfoFlow(packageName, version) - }.collectAsStateWithLifecycle(initialValue = emptyList()) + val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList()) val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState() - val patches by remember { - derivedStateOf { - vm.getPatches(bundles, allowIncompatiblePatches) - } + val patches = remember(bundles, allowIncompatiblePatches) { + vm.getPatches(bundles, allowIncompatiblePatches) } - val selectedPatchCount by remember { - derivedStateOf { - patches.values.sumOf { it.size } - } + val selectedPatchCount = remember(patches) { + patches.values.sumOf { it.size } } val launcher = rememberLauncherForActivityResult( @@ -77,6 +72,7 @@ fun SelectedAppInfoScreen( EventEffect(flow = vm.launchActivityFlow) { intent -> launcher.launch(intent) } + val composableScope = rememberCoroutineScope() val error by vm.errorFlow.collectAsStateWithLifecycle(null) Scaffold( @@ -103,11 +99,19 @@ fun SelectedAppInfoScreen( return@patchClick } - onPatchClick( - vm.selectedApp, - patches, - vm.getOptionsFiltered(bundles) - ) + + composableScope.launch { + if (!vm.hasSetRequiredOptions(patches)) { + onRequiredOptions( + vm.selectedApp, + vm.getCustomPatches(bundles, allowIncompatiblePatches), + vm.options + ) + return@launch + } + + onPatchClick() + } } ) } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt index ff815266c2..31f6d6437f 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt @@ -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.Extensions.bundleInfoFlow import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection -import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo import app.revanced.manager.util.Options import app.revanced.manager.util.PatchSelection @@ -37,11 +36,14 @@ import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.get import kotlinx.collections.immutable.* +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map -@Stable @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 savedStateHandle: SavedStateHandle = get() private val prefs: PreferencesManager = get() @@ -114,6 +116,22 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi 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) = bundles.any { bundle -> bundle.patchSequence(allowIncompatiblePatches).any { patch -> isSelected(bundle.uid, patch) diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index cdf872674c..302b5e89c9 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -9,6 +9,7 @@ import android.util.Log import androidx.activity.result.ActivityResult import androidx.annotation.StringRes import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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.network.downloader.LoadedDownloaderPlugin 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.PluginHostApi import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.ui.model.BundleInfo import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection +import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet 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.util.Options import app.revanced.manager.util.PM @@ -63,7 +67,6 @@ class SelectedAppInfoViewModel( input: SelectedApplicationInfo.ViewModelParams ) : ViewModel(), KoinComponent { private val app: Application = get() - val bundlesRepo: PatchBundleRepository = get() private val bundleRepository: PatchBundleRepository = get() private val selectionRepository: PatchSelectionRepository = get() private val optionsRepository: PatchOptionsRepository = get() @@ -174,6 +177,10 @@ class SelectedAppInfoViewModel( } } + val bundleInfoFlow by derivedStateOf { + bundleRepository.bundleInfoFlow(packageName, selectedApp.version) + } + fun showSourceSelector() { dismissSourceSelector() showSourceSelector = true @@ -260,6 +267,23 @@ class SelectedAppInfoViewModel( 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) = options.filtered(bundles) fun getPatches(bundles: List, allowUnsupported: Boolean) = @@ -272,7 +296,7 @@ class SelectedAppInfoViewModel( (selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported) 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 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f9ee7dae77..b24cce6217 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -365,6 +365,8 @@ Invalid date Disable battery optimization Invalid value + This option is required + Required options Failed to check for updates: %s No update available