diff --git a/app/src/main/java/app/revanced/manager/ui/component/CheckedFilterChip.kt b/app/src/main/java/app/revanced/manager/ui/component/CheckedFilterChip.kt new file mode 100644 index 0000000000..a81c456f7b --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/CheckedFilterChip.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/SearchBar.kt b/app/src/main/java/app/revanced/manager/ui/component/SearchBar.kt new file mode 100644 index 0000000000..7c48b8121c --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/SearchBar.kt @@ -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 + ) + } +} \ No newline at end of file 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 69e0d45eb7..d1fddd9ff7 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 @@ -1,16 +1,50 @@ 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.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.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.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.outlined.HelpOutline -import androidx.compose.material.icons.outlined.* -import androidx.compose.material3.* +import androidx.compose.material.icons.filled.Close +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.derivedStateOf import androidx.compose.runtime.getValue @@ -19,8 +53,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.rotate import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -31,9 +67,10 @@ import app.revanced.manager.R import app.revanced.manager.patcher.patch.Option import app.revanced.manager.patcher.patch.PatchInfo 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.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.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticTab @@ -41,14 +78,13 @@ import app.revanced.manager.ui.component.patches.OptionItem 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.util.Options import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.isScrollingUp import app.revanced.manager.util.transparentListItemColors import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun PatchesSelectorScreen( onSave: (PatchSelection?, Options) -> Unit, @@ -63,20 +99,17 @@ fun PatchesSelectorScreen( bundles.size } val composableScope = rememberCoroutineScope() - var search: String? by rememberSaveable { - mutableStateOf(null) + val (query, setQuery) = rememberSaveable { + mutableStateOf("") + } + val (searchExpanded, setSearchExpanded) = rememberSaveable { + mutableStateOf(false) } var showBottomSheet by rememberSaveable { mutableStateOf(false) } val showSaveButton by remember { derivedStateOf { vm.selectionIsValid(bundles) } } - val availablePatchCount by remember { - derivedStateOf { - bundles.sumOf { it.patchCount } - } - } - val defaultPatchSelectionCount by vm.defaultSelectionCount .collectAsStateWithLifecycle(initialValue = 0) @@ -108,27 +141,22 @@ fun PatchesSelectorScreen( style = MaterialTheme.typography.titleMedium ) - Row( + FlowRow( 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, onClick = { vm.toggleFlag(SHOW_SUPPORTED) }, label = { Text(stringResource(R.string.supported)) } ) - FilterChip( + CheckedFilterChip( selected = vm.filter and SHOW_UNIVERSAL != 0, onClick = { vm.toggleFlag(SHOW_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( uid: Int, patches: List, - filterFlag: Int, + visible: Boolean, supported: Boolean, header: (@Composable () -> Unit)? = null ) { - if (patches.isNotEmpty() && (vm.filter and filterFlag) != 0 || vm.filter == 0) { + if (patches.isNotEmpty() && visible) { header?.let { - item { + item(contentType = 0) { it() } } items( items = patches, - key = { it.name } + key = { it.name }, + contentType = { 1 } ) { patch -> PatchItem( 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.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), + Scaffold( + topBar = { + SearchBar( + query = query, + onQueryChange = setQuery, + expanded = searchExpanded, + onExpandedChange = setSearchExpanded, + placeholder = { + Text(stringResource(R.string.search_patches)) + }, + leadingIcon = { + val rotation by animateFloatAsState( + targetValue = if (searchExpanded) 360f else 0f, + animationSpec = tween(durationMillis = 400, easing = EaseInOut), + label = "SearchBar back button" ) + IconButton( + onClick = { + if (searchExpanded) { + setSearchExpanded(false) + } else { + onBackClick() + } + } + ) { + 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] - if (!vm.allowIncompatiblePatches) return@LazyColumnWithScrollbar - patchList( - uid = bundle.uid, - patches = bundle.unsupported.searched(), - filterFlag = SHOW_UNSUPPORTED, - supported = true + LazyColumnWithScrollbar( + modifier = Modifier.fillMaxSize() ) { - ListHeader( - title = stringResource(R.string.unsupported_patches), - onHelpClick = { showUnsupportedPatchesDialog = true } - ) - } - } - } - } - - Scaffold( - topBar = { - AppTopBar( - title = stringResource( - R.string.patches_selected, - selectedPatchCount, - availablePatchCount - ), - onBackClick = onBackClick, - actions = { - IconButton(onClick = vm::reset) { - Icon(Icons.Outlined.Restore, stringResource(R.string.reset)) + fun List.searched() = filter { + it.name.contains(query, true) } - IconButton(onClick = { showBottomSheet = true }) { - Icon(Icons.Outlined.FilterList, stringResource(R.string.more)) + + 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), + ) } - IconButton( - onClick = { - search = "" - } + + patchList( + uid = bundle.uid, + patches = bundle.unsupported.searched(), + visible = vm.filter and SHOW_SUPPORTED == 0, + supported = vm.allowIncompatiblePatches ) { - Icon(Icons.Outlined.Search, stringResource(R.string.search)) + ListHeader( + title = stringResource(R.string.unsupported_patches), + onHelpClick = { showUnsupportedPatchesDialog = true } + ) } } - ) + } }, floatingActionButton = { if (!showSaveButton) return@Scaffold - HapticExtendedFloatingActionButton( - text = { Text(stringResource(R.string.save)) }, - icon = { - Icon( - Icons.Outlined.Save, - stringResource(R.string.save) + AnimatedVisibility( + visible = !searchExpanded, + enter = slideInHorizontally { it } + fadeIn(), + exit = slideOutHorizontally { it } + fadeOut() + ) { + 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 -> Column( - Modifier + modifier = Modifier .fillMaxSize() .padding(paddingValues) ) { @@ -359,13 +428,13 @@ fun PatchesSelectorScreen( patchList( uid = bundle.uid, patches = bundle.supported, - filterFlag = SHOW_SUPPORTED, + visible = true, supported = true ) patchList( uid = bundle.uid, patches = bundle.universal, - filterFlag = SHOW_UNIVERSAL, + visible = vm.filter and SHOW_UNIVERSAL != 0, supported = true ) { ListHeader( @@ -375,7 +444,7 @@ fun PatchesSelectorScreen( patchList( uid = bundle.uid, patches = bundle.unsupported, - filterFlag = SHOW_UNSUPPORTED, + visible = vm.filter and SHOW_SUPPORTED == 0, supported = vm.allowIncompatiblePatches ) { ListHeader( 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 31f6d6437f..976de5431e 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 @@ -2,6 +2,7 @@ package app.revanced.manager.ui.viewmodel import android.app.Application import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.setValue @@ -104,7 +105,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi val compatibleVersions = mutableStateListOf() - var filter by mutableIntStateOf(0) + var filter by mutableIntStateOf(SHOW_SUPPORTED xor SHOW_UNIVERSAL) private set private val defaultPatchSelection = bundlesFlow.map { bundles -> @@ -220,7 +221,6 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi companion object { const val SHOW_SUPPORTED = 1 // 2^0 const val SHOW_UNIVERSAL = 2 // 2^1 - const val SHOW_UNSUPPORTED = 4 // 2^2 private val optionsSaver: Saver = snapshotStateMapSaver( // Patch name -> Options diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b24cce6217..025b19733b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -164,6 +164,7 @@ Warning Add Close + Clear System Light Dark @@ -210,7 +211,7 @@ No patched apps found Tap on the patches to get more information about them %s selected - Unsupported patches + Incompatible patches Universal patches Patch selection and options has been reset to recommended defaults Patch options have been reset @@ -219,10 +220,10 @@ Stop using defaults? 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. 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. - Supported - Universal + This version + Any app Unsupported - Patch name + Search patches This patch is not compatible with the selected app version (%1$s).\n\nIt only supports the following version(s): %2$s. Continue with this version? Not all patches support this version (%s). Do you want to continue anyway? @@ -354,6 +355,7 @@ Failed to download update: %s Cancel Save + Save (%1$s) Update Empty Tap on Update when prompted. \n ReVanced Manager will close when updating.