feat: Redesign the patches screen (#2381)

This commit is contained in:
Tornike Khintibidze 2025-01-18 20:03:38 +04:00 committed by GitHub
parent 0ab424bfdb
commit 74338931b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 311 additions and 121 deletions

View File

@ -0,0 +1,61 @@
package app.revanced.manager.ui.component
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandIn
import androidx.compose.animation.shrinkOut
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.SelectableChipColors
import androidx.compose.material3.SelectableChipElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
@Composable
fun CheckedFilterChip(
selected: Boolean,
onClick: () -> Unit,
label: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
trailingIcon: @Composable (() -> Unit)? = null,
shape: Shape = FilterChipDefaults.shape,
colors: SelectableChipColors = FilterChipDefaults.filterChipColors(),
elevation: SelectableChipElevation? = FilterChipDefaults.filterChipElevation(),
border: BorderStroke? = FilterChipDefaults.filterChipBorder(enabled, selected),
interactionSource: MutableInteractionSource? = null
) {
FilterChip(
selected = selected,
onClick = onClick,
label = label,
modifier = modifier,
enabled = enabled,
leadingIcon = {
AnimatedVisibility(
visible = selected,
enter = expandIn(expandFrom = Alignment.CenterStart),
exit = shrinkOut(shrinkTowards = Alignment.CenterStart)
) {
Icon(
modifier = Modifier.size(FilterChipDefaults.IconSize),
imageVector = Icons.Filled.Done,
contentDescription = null,
)
}
},
trailingIcon = trailingIcon,
shape = shape,
colors = colors,
elevation = elevation,
border = border,
interactionSource = interactionSource
)
}

View File

@ -0,0 +1,60 @@
package app.revanced.manager.ui.component
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarColors
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBar(
query: String,
onQueryChange: (String) -> Unit,
expanded: Boolean,
onExpandedChange: (Boolean) -> Unit,
placeholder: (@Composable () -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit
) {
val colors = SearchBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
dividerColor = MaterialTheme.colorScheme.outline
)
val keyboardController = LocalSoftwareKeyboardController.current
Box(modifier = Modifier.fillMaxWidth()) {
SearchBar(
modifier = Modifier.align(Alignment.Center),
inputField = {
SearchBarDefaults.InputField(
modifier = Modifier.sizeIn(minWidth = 380.dp),
query = query,
onQueryChange = onQueryChange,
onSearch = {
keyboardController?.hide()
},
expanded = expanded,
onExpandedChange = onExpandedChange,
placeholder = placeholder,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon
)
},
expanded = expanded,
onExpandedChange = onExpandedChange,
colors = colors,
content = content
)
}
}

View File

@ -1,16 +1,50 @@
package app.revanced.manager.ui.screen package app.revanced.manager.ui.screen
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.EaseInOut
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.* import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -19,8 +53,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -31,24 +67,24 @@ import app.revanced.manager.R
import app.revanced.manager.patcher.patch.Option import app.revanced.manager.patcher.patch.Option
import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.CheckedFilterChip
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.SafeguardDialog import app.revanced.manager.ui.component.SafeguardDialog
import app.revanced.manager.ui.component.SearchView import app.revanced.manager.ui.component.SearchBar
import app.revanced.manager.ui.component.haptics.HapticCheckbox import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticTab import app.revanced.manager.ui.component.haptics.HapticTab
import app.revanced.manager.ui.component.patches.OptionItem import app.revanced.manager.ui.component.patches.OptionItem
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.isScrollingUp import app.revanced.manager.util.isScrollingUp
import app.revanced.manager.util.transparentListItemColors import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun PatchesSelectorScreen( fun PatchesSelectorScreen(
onSave: (PatchSelection?, Options) -> Unit, onSave: (PatchSelection?, Options) -> Unit,
@ -63,20 +99,17 @@ fun PatchesSelectorScreen(
bundles.size bundles.size
} }
val composableScope = rememberCoroutineScope() val composableScope = rememberCoroutineScope()
var search: String? by rememberSaveable { val (query, setQuery) = rememberSaveable {
mutableStateOf(null) mutableStateOf("")
}
val (searchExpanded, setSearchExpanded) = rememberSaveable {
mutableStateOf(false)
} }
var showBottomSheet by rememberSaveable { mutableStateOf(false) } var showBottomSheet by rememberSaveable { mutableStateOf(false) }
val showSaveButton by remember { val showSaveButton by remember {
derivedStateOf { vm.selectionIsValid(bundles) } derivedStateOf { vm.selectionIsValid(bundles) }
} }
val availablePatchCount by remember {
derivedStateOf {
bundles.sumOf { it.patchCount }
}
}
val defaultPatchSelectionCount by vm.defaultSelectionCount val defaultPatchSelectionCount by vm.defaultSelectionCount
.collectAsStateWithLifecycle(initialValue = 0) .collectAsStateWithLifecycle(initialValue = 0)
@ -108,27 +141,22 @@ fun PatchesSelectorScreen(
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
Row( FlowRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp) horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
FilterChip( CheckedFilterChip(
selected = vm.filter and SHOW_SUPPORTED != 0, selected = vm.filter and SHOW_UNSUPPORTED == 0,
onClick = { vm.toggleFlag(SHOW_SUPPORTED) }, onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
label = { Text(stringResource(R.string.supported)) } label = { Text(stringResource(R.string.supported)) }
) )
FilterChip( CheckedFilterChip(
selected = vm.filter and SHOW_UNIVERSAL != 0, selected = vm.filter and SHOW_UNIVERSAL != 0,
onClick = { vm.toggleFlag(SHOW_UNIVERSAL) }, onClick = { vm.toggleFlag(SHOW_UNIVERSAL) },
label = { Text(stringResource(R.string.universal)) }, label = { Text(stringResource(R.string.universal)) },
) )
FilterChip(
selected = vm.filter and SHOW_UNSUPPORTED != 0,
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
label = { Text(stringResource(R.string.unsupported)) },
)
} }
} }
} }
@ -175,20 +203,21 @@ fun PatchesSelectorScreen(
fun LazyListScope.patchList( fun LazyListScope.patchList(
uid: Int, uid: Int,
patches: List<PatchInfo>, patches: List<PatchInfo>,
filterFlag: Int, visible: Boolean,
supported: Boolean, supported: Boolean,
header: (@Composable () -> Unit)? = null header: (@Composable () -> Unit)? = null
) { ) {
if (patches.isNotEmpty() && (vm.filter and filterFlag) != 0 || vm.filter == 0) { if (patches.isNotEmpty() && visible) {
header?.let { header?.let {
item { item(contentType = 0) {
it() it()
} }
} }
items( items(
items = patches, items = patches,
key = { it.name } key = { it.name },
contentType = { 1 }
) { patch -> ) { patch ->
PatchItem( PatchItem(
patch = patch, patch = patch,
@ -222,102 +251,142 @@ fun PatchesSelectorScreen(
} }
} }
search?.let { query ->
SearchView(
query = query,
onQueryChange = { search = it },
onActiveChange = { if (!it) search = null },
placeholder = { Text(stringResource(R.string.search_patches)) }
) {
val bundle = bundles[pagerState.currentPage]
LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize()
) {
fun List<PatchInfo>.searched() = filter {
it.name.contains(query, true)
}
patchList(
uid = bundle.uid,
patches = bundle.supported.searched(),
filterFlag = SHOW_SUPPORTED,
supported = true
)
patchList(
uid = bundle.uid,
patches = bundle.universal.searched(),
filterFlag = SHOW_UNIVERSAL,
supported = true
) {
ListHeader(
title = stringResource(R.string.universal_patches),
)
}
if (!vm.allowIncompatiblePatches) return@LazyColumnWithScrollbar
patchList(
uid = bundle.uid,
patches = bundle.unsupported.searched(),
filterFlag = SHOW_UNSUPPORTED,
supported = true
) {
ListHeader(
title = stringResource(R.string.unsupported_patches),
onHelpClick = { showUnsupportedPatchesDialog = true }
)
}
}
}
}
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( SearchBar(
title = stringResource( query = query,
R.string.patches_selected, onQueryChange = setQuery,
selectedPatchCount, expanded = searchExpanded,
availablePatchCount onExpandedChange = setSearchExpanded,
), placeholder = {
onBackClick = onBackClick, Text(stringResource(R.string.search_patches))
actions = { },
IconButton(onClick = vm::reset) { leadingIcon = {
Icon(Icons.Outlined.Restore, stringResource(R.string.reset)) val rotation by animateFloatAsState(
} targetValue = if (searchExpanded) 360f else 0f,
IconButton(onClick = { showBottomSheet = true }) { animationSpec = tween(durationMillis = 400, easing = EaseInOut),
Icon(Icons.Outlined.FilterList, stringResource(R.string.more)) label = "SearchBar back button"
} )
IconButton( IconButton(
onClick = { onClick = {
search = "" if (searchExpanded) {
setSearchExpanded(false)
} else {
onBackClick()
}
} }
) { ) {
Icon(Icons.Outlined.Search, stringResource(R.string.search)) Icon(
modifier = Modifier.rotate(rotation),
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
},
trailingIcon = {
AnimatedContent(
targetState = searchExpanded,
label = "Filter/Clear",
transitionSpec = { fadeIn() togetherWith fadeOut() }
) { searchExpanded ->
if (searchExpanded) {
IconButton(
onClick = { setQuery("") },
enabled = query.isNotEmpty()
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.clear)
)
}
} else {
IconButton(onClick = { showBottomSheet = true }) {
Icon(
imageVector = Icons.Outlined.FilterList,
contentDescription = stringResource(R.string.more)
)
}
}
} }
} }
) ) {
val bundle = bundles[pagerState.currentPage]
LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize()
) {
fun List<PatchInfo>.searched() = filter {
it.name.contains(query, true)
}
patchList(
uid = bundle.uid,
patches = bundle.supported.searched(),
visible = true,
supported = true
)
patchList(
uid = bundle.uid,
patches = bundle.universal.searched(),
visible = vm.filter and SHOW_UNIVERSAL != 0,
supported = true
) {
ListHeader(
title = stringResource(R.string.universal_patches),
)
}
patchList(
uid = bundle.uid,
patches = bundle.unsupported.searched(),
visible = vm.filter and SHOW_UNSUPPORTED != 0,
supported = vm.allowIncompatiblePatches
) {
ListHeader(
title = stringResource(R.string.unsupported_patches),
onHelpClick = { showUnsupportedPatchesDialog = true }
)
}
}
}
}, },
floatingActionButton = { floatingActionButton = {
if (!showSaveButton) return@Scaffold if (!showSaveButton) return@Scaffold
HapticExtendedFloatingActionButton( AnimatedVisibility(
text = { Text(stringResource(R.string.save)) }, visible = !searchExpanded,
icon = { enter = slideInHorizontally { it } + fadeIn(),
Icon( exit = slideOutHorizontally { it } + fadeOut()
Icons.Outlined.Save, ) {
stringResource(R.string.save) Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
SmallFloatingActionButton(
onClick = vm::reset,
containerColor = MaterialTheme.colorScheme.tertiaryContainer
) {
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
}
HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.save_with_count, selectedPatchCount)) },
icon = {
Icon(
imageVector = Icons.Outlined.Save,
contentDescription = stringResource(R.string.save)
)
},
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp ?: true,
onClick = {
onSave(vm.getCustomSelection(), vm.getOptions())
}
) )
},
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
?: true,
onClick = {
onSave(vm.getCustomSelection(), vm.getOptions())
} }
) }
} }
) { paddingValues -> ) { paddingValues ->
Column( Column(
Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) { ) {
@ -359,13 +428,13 @@ fun PatchesSelectorScreen(
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.supported, patches = bundle.supported,
filterFlag = SHOW_SUPPORTED, visible = true,
supported = true supported = true
) )
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.universal, patches = bundle.universal,
filterFlag = SHOW_UNIVERSAL, visible = vm.filter and SHOW_UNIVERSAL != 0,
supported = true supported = true
) { ) {
ListHeader( ListHeader(
@ -375,7 +444,7 @@ fun PatchesSelectorScreen(
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.unsupported, patches = bundle.unsupported,
filterFlag = SHOW_UNSUPPORTED, visible = vm.filter and SHOW_UNSUPPORTED != 0,
supported = vm.allowIncompatiblePatches supported = vm.allowIncompatiblePatches
) { ) {
ListHeader( ListHeader(

View File

@ -1,7 +1,6 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -104,7 +103,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
val compatibleVersions = mutableStateListOf<String>() val compatibleVersions = mutableStateListOf<String>()
var filter by mutableIntStateOf(0) var filter by mutableIntStateOf(SHOW_UNIVERSAL)
private set private set
private val defaultPatchSelection = bundlesFlow.map { bundles -> private val defaultPatchSelection = bundlesFlow.map { bundles ->
@ -218,9 +217,8 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
} }
companion object { companion object {
const val SHOW_SUPPORTED = 1 // 2^0 const val SHOW_UNSUPPORTED = 1 // 2^0
const val SHOW_UNIVERSAL = 2 // 2^1 const val SHOW_UNIVERSAL = 2 // 2^1
const val SHOW_UNSUPPORTED = 4 // 2^2
private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver( private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver(
// Patch name -> Options // Patch name -> Options

View File

@ -164,6 +164,7 @@
<string name="warning">Warning</string> <string name="warning">Warning</string>
<string name="add">Add</string> <string name="add">Add</string>
<string name="close">Close</string> <string name="close">Close</string>
<string name="clear">Clear</string>
<string name="system">System</string> <string name="system">System</string>
<string name="light">Light</string> <string name="light">Light</string>
<string name="dark">Dark</string> <string name="dark">Dark</string>
@ -210,7 +211,7 @@
<string name="no_patched_apps_found">No patched apps found</string> <string name="no_patched_apps_found">No patched apps found</string>
<string name="tap_on_patches">Tap on the patches to get more information about them</string> <string name="tap_on_patches">Tap on the patches to get more information about them</string>
<string name="bundles_selected">%s selected</string> <string name="bundles_selected">%s selected</string>
<string name="unsupported_patches">Unsupported patches</string> <string name="unsupported_patches">Incompatible patches</string>
<string name="universal_patches">Universal patches</string> <string name="universal_patches">Universal patches</string>
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string> <string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
<string name="patch_options_reset_toast">Patch options have been reset</string> <string name="patch_options_reset_toast">Patch options have been reset</string>
@ -219,10 +220,10 @@
<string name="selection_warning_title">Stop using defaults?</string> <string name="selection_warning_title">Stop using defaults?</string>
<string name="selection_warning_description">It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on \"Allow changing patch selection\" in the advanced settings before toggling patches.</string> <string name="selection_warning_description">It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on \"Allow changing patch selection\" in the advanced settings before toggling patches.</string>
<string name="universal_patch_warning_description">Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.\n\nThis warning can be disabled in the advanced settings.</string> <string name="universal_patch_warning_description">Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.\n\nThis warning can be disabled in the advanced settings.</string>
<string name="supported">Supported</string> <string name="supported">This version</string>
<string name="universal">Universal</string> <string name="universal">Any app</string>
<string name="unsupported">Unsupported</string> <string name="unsupported">Unsupported</string>
<string name="search_patches">Patch name</string> <string name="search_patches">Search patches</string>
<string name="app_not_supported">This patch is not compatible with the selected app version (%1$s).\n\nIt only supports the following version(s): %2$s.</string> <string name="app_not_supported">This patch is not compatible with the selected app version (%1$s).\n\nIt only supports the following version(s): %2$s.</string>
<string name="continue_with_version">Continue with this version?</string> <string name="continue_with_version">Continue with this version?</string>
<string name="version_not_supported">Not all patches support this version (%s). Do you want to continue anyway?</string> <string name="version_not_supported">Not all patches support this version (%s). Do you want to continue anyway?</string>
@ -354,6 +355,7 @@
<string name="download_manager_failed">Failed to download update: %s</string> <string name="download_manager_failed">Failed to download update: %s</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="save">Save</string> <string name="save">Save</string>
<string name="save_with_count">Save (%1$s)</string>
<string name="update">Update</string> <string name="update">Update</string>
<string name="empty">Empty</string> <string name="empty">Empty</string>
<string name="installing_message">Tap on <b>Update</b> when prompted. \n ReVanced Manager will close when updating.</string> <string name="installing_message">Tap on <b>Update</b> when prompted. \n ReVanced Manager will close when updating.</string>