mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-04-30 05:54:26 +02:00
feat: add required options screen
This commit is contained in:
parent
9db3bd5b3f
commit
7e4f5653d3
@ -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 = {
|
||||||
|
it.lifecycleScope.launch {
|
||||||
navController.navigateComplex(
|
navController.navigateComplex(
|
||||||
Patcher,
|
Patcher,
|
||||||
Patcher.ViewModelParams(app, patches, options)
|
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) {
|
||||||
|
@ -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() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,10 @@ 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(
|
||||||
|
allowUnsupported: Boolean,
|
||||||
|
condition: (Int, PatchInfo) -> Boolean
|
||||||
|
): PatchSelection = this.associate { bundle ->
|
||||||
val patches =
|
val patches =
|
||||||
bundle.patchSequence(allowUnsupported)
|
bundle.patchSequence(allowUnsupported)
|
||||||
.mapNotNullTo(mutableSetOf()) { patch ->
|
.mapNotNullTo(mutableSetOf()) { patch ->
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,21 +55,15 @@ 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 = remember(patches) {
|
||||||
val selectedPatchCount by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
patches.values.sumOf { it.size }
|
patches.values.sumOf { it.size }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val launcher = rememberLauncherForActivityResult(
|
val launcher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult(),
|
contract = ActivityResultContracts.StartActivityForResult(),
|
||||||
@ -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(
|
|
||||||
|
composableScope.launch {
|
||||||
|
if (!vm.hasSetRequiredOptions(patches)) {
|
||||||
|
onRequiredOptions(
|
||||||
vm.selectedApp,
|
vm.selectedApp,
|
||||||
patches,
|
vm.getCustomPatches(bundles, allowIncompatiblePatches),
|
||||||
vm.getOptionsFiltered(bundles)
|
vm.options
|
||||||
)
|
)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
onPatchClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user