@@ -15,9 +15,12 @@ import androidx.compose.foundation.layout.Arrangement
1515import androidx.compose.foundation.layout.Column
1616import androidx.compose.foundation.layout.ExperimentalLayoutApi
1717import androidx.compose.foundation.layout.FlowRow
18+ import androidx.compose.foundation.layout.Row
19+ import androidx.compose.foundation.layout.Spacer
1820import androidx.compose.foundation.layout.fillMaxSize
1921import androidx.compose.foundation.layout.fillMaxWidth
2022import androidx.compose.foundation.layout.padding
23+ import androidx.compose.foundation.layout.width
2124import androidx.compose.foundation.lazy.LazyListScope
2225import androidx.compose.foundation.lazy.LazyListState
2326import androidx.compose.foundation.lazy.items
@@ -27,13 +30,16 @@ import androidx.compose.material.icons.Icons
2730import androidx.compose.material.icons.automirrored.filled.ArrowBack
2831import androidx.compose.material.icons.automirrored.outlined.HelpOutline
2932import androidx.compose.material.icons.filled.Close
33+ import androidx.compose.material.icons.outlined.Deselect
3034import androidx.compose.material.icons.outlined.FilterList
3135import androidx.compose.material.icons.outlined.Restore
3236import androidx.compose.material.icons.outlined.Save
3337import androidx.compose.material.icons.outlined.Settings
38+ import androidx.compose.material.icons.outlined.SwapHoriz
3439import androidx.compose.material.icons.outlined.WarningAmber
3540import androidx.compose.material3.AlertDialog
3641import androidx.compose.material3.ExperimentalMaterial3Api
42+ import androidx.compose.material3.HorizontalDivider
3743import androidx.compose.material3.Icon
3844import androidx.compose.material3.IconButton
3945import androidx.compose.material3.ListItem
@@ -60,7 +66,9 @@ import androidx.compose.ui.Modifier
6066import androidx.compose.ui.draw.alpha
6167import androidx.compose.ui.draw.rotate
6268import androidx.compose.ui.res.stringResource
69+ import androidx.compose.ui.state.ToggleableState
6370import androidx.compose.ui.unit.dp
71+ import androidx.compose.ui.graphics.vector.ImageVector
6472import androidx.lifecycle.compose.collectAsStateWithLifecycle
6573import app.revanced.manager.R
6674import app.revanced.manager.patcher.patch.Option
@@ -74,6 +82,7 @@ import app.revanced.manager.ui.component.SearchBar
7482import app.revanced.manager.ui.component.haptics.HapticCheckbox
7583import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
7684import app.revanced.manager.ui.component.haptics.HapticTab
85+ import app.revanced.manager.ui.component.haptics.HapticTriStateCheckbox
7786import app.revanced.manager.ui.component.patches.OptionItem
7887import app.revanced.manager.ui.component.patches.SelectionWarningDialog
7988import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
@@ -124,7 +133,39 @@ fun PatchesSelectorScreen(
124133
125134 val patchLazyListStates = remember(bundles) { List (bundles.size) { LazyListState () } }
126135
136+ var showSelectionWarning by rememberSaveable { mutableStateOf(false ) }
137+ var showUniversalWarning by rememberSaveable { mutableStateOf(false ) }
138+
139+ var pendingScopeAction by remember { mutableStateOf< ((Int? ) -> Unit )? > (null ) }
140+
141+ fun executeScopedAction (action : (Int? ) -> Unit ) {
142+ if (bundles.size > 1 ) {
143+ pendingScopeAction = action
144+ } else {
145+ action(bundles.firstOrNull()?.uid)
146+ }
147+ }
148+
149+ pendingScopeAction?.let { action ->
150+ val currentBundle = bundles.getOrNull(pagerState.currentPage) ? : return @let
151+
152+ ScopeDialog (
153+ bundleName = currentBundle.name,
154+ onDismissRequest = { pendingScopeAction = null },
155+ onAllPatches = {
156+ action(null )
157+ pendingScopeAction = null
158+ },
159+ onBundleOnly = {
160+ action(currentBundle.uid)
161+ pendingScopeAction = null
162+ }
163+ )
164+ }
165+
127166 if (showBottomSheet) {
167+ val currentBundle = bundles.getOrNull(pagerState.currentPage)
168+
128169 ModalBottomSheet (
129170 onDismissRequest = {
130171 showBottomSheet = false
@@ -161,6 +202,71 @@ fun PatchesSelectorScreen(
161202 label = { Text (stringResource(R .string.universal)) },
162203 )
163204 }
205+
206+ HorizontalDivider (modifier = Modifier .padding(vertical = 16 .dp))
207+
208+ Text (
209+ text = stringResource(R .string.patch_selector_sheet_actions_title),
210+ style = MaterialTheme .typography.headlineSmall,
211+ modifier = Modifier .padding(bottom = 8 .dp)
212+ )
213+
214+ fun guardedAction (action : () -> Unit ) {
215+ showBottomSheet = false
216+ if (viewModel.selectionWarningEnabled) {
217+ showSelectionWarning = true
218+ } else {
219+ action()
220+ }
221+ }
222+
223+ ActionItem (
224+ icon = Icons .Outlined .Restore ,
225+ text = stringResource(R .string.restore_default_selection),
226+ onClick = {
227+ guardedAction {
228+ executeScopedAction { uid ->
229+ viewModel.restoreDefaults(uid)
230+ }
231+ }
232+ }
233+ )
234+
235+ ActionItem (
236+ icon = Icons .Outlined .Deselect ,
237+ text = stringResource(R .string.deselect_all),
238+ onClick = {
239+ guardedAction {
240+ executeScopedAction { uid ->
241+ viewModel.deselectAll(bundles, uid)
242+ }
243+ }
244+ }
245+ )
246+
247+ ActionItem (
248+ icon = Icons .Outlined .SwapHoriz ,
249+ text = stringResource(R .string.invert_selection),
250+ onClick = {
251+ guardedAction {
252+ executeScopedAction { uid ->
253+ viewModel.invertSelection(bundles, uid)
254+ }
255+ }
256+ }
257+ )
258+
259+ if (bundles.size > 1 && currentBundle != null ) {
260+ ActionItem (
261+ icon = Icons .Outlined .Deselect ,
262+ text = stringResource(R .string.deselect_all_except, currentBundle.name),
263+ onClick = {
264+ guardedAction {
265+ viewModel.deselectAllExcept(bundles, currentBundle.uid)
266+ }
267+ }
268+ )
269+ }
164270 }
165271 }
166272 }
@@ -191,9 +297,6 @@ fun PatchesSelectorScreen(
191297 )
192298 }
193299
194- var showSelectionWarning by rememberSaveable { mutableStateOf(false ) }
195- var showUniversalWarning by rememberSaveable { mutableStateOf(false ) }
196-
197300 if (showSelectionWarning)
198301 SelectionWarningDialog (onDismiss = { showSelectionWarning = false })
199302
@@ -426,16 +529,38 @@ fun PatchesSelectorScreen(
426529 }
427530 },
428531 text = {
429- Column (horizontalAlignment = Alignment .CenterHorizontally ) {
430- Text (
431- text = bundle.name,
432- style = MaterialTheme .typography.bodyMedium
433- )
434- Text (
435- text = bundle.version.orEmpty(),
436- style = MaterialTheme .typography.bodySmall,
437- color = MaterialTheme .colorScheme.onSurfaceVariant
532+ Row (verticalAlignment = Alignment .CenterVertically ) {
533+ val selectionState = viewModel.getBundleSelectionState(bundle)
534+ val toggleableState = when (selectionState) {
535+ true -> ToggleableState .On
536+ false -> ToggleableState .Off
537+ null -> ToggleableState .Indeterminate
538+ }
539+
540+ HapticTriStateCheckbox (
541+ state = toggleableState,
542+ onClick = {
543+ when {
544+ viewModel.selectionWarningEnabled -> showSelectionWarning = true
545+ selectionState == false -> viewModel.restoreDefaults(bundle.uid)
546+ else -> viewModel.deselectAll(bundles, bundle.uid)
547+ }
548+ }
438549 )
550+
551+ Spacer (modifier = Modifier .width(4 .dp))
552+
553+ Column (horizontalAlignment = Alignment .CenterHorizontally ) {
554+ Text (
555+ text = bundle.name,
556+ style = MaterialTheme .typography.bodyMedium
557+ )
558+ Text (
559+ text = bundle.version.orEmpty(),
560+ style = MaterialTheme .typography.bodySmall,
561+ color = MaterialTheme .colorScheme.onSurfaceVariant
562+ )
563+ }
439564 }
440565 },
441566 selectedContentColor = MaterialTheme .colorScheme.primary,
@@ -612,6 +737,41 @@ private fun IncompatiblePatchDialog(
612737 }
613738)
614739
740+ @Composable
741+ private fun ActionItem (
742+ icon : ImageVector ,
743+ text : String ,
744+ onClick : () -> Unit
745+ ) {
746+ ListItem (
747+ modifier = Modifier .clickable(onClick = onClick),
748+ leadingContent = { Icon (icon, contentDescription = null ) },
749+ headlineContent = { Text (text) },
750+ colors = transparentListItemColors
751+ )
752+ }
753+
754+ @Composable
755+ private fun ScopeDialog (
756+ bundleName : String ,
757+ onDismissRequest : () -> Unit ,
758+ onAllPatches : () -> Unit ,
759+ onBundleOnly : () -> Unit
760+ ) = AlertDialog (
761+ onDismissRequest = onDismissRequest,
762+ title = { Text (stringResource(R .string.scope_dialog_title)) },
763+ confirmButton = {
764+ TextButton (onClick = onAllPatches) {
765+ Text (stringResource(R .string.scope_all_patches))
766+ }
767+ },
768+ dismissButton = {
769+ TextButton (onClick = onBundleOnly) {
770+ Text (stringResource(R .string.scope_bundle_patches, bundleName))
771+ }
772+ }
773+ )
774+
615775@OptIn(ExperimentalMaterial3Api ::class )
616776@Composable
617777private fun OptionsDialog (
0 commit comments