feat: Migrate more buttons

This commit is contained in:
Ushie 2024-07-26 02:27:06 +03:00
parent bfbf27024d
commit 968c665791
No known key found for this signature in database
GPG key ID: B3AAD18842E34632
7 changed files with 306 additions and 272 deletions

View file

@ -10,26 +10,9 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Topic import androidx.compose.material.icons.filled.Topic
import androidx.compose.material3.Checkbox import androidx.compose.material3.*
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.*
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
@ -37,6 +20,8 @@ import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.AlertDialogExtended import app.revanced.manager.ui.component.AlertDialogExtended
import app.revanced.manager.ui.component.TextHorizontalPadding import app.revanced.manager.ui.component.TextHorizontalPadding
import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.model.BundleType import app.revanced.manager.ui.model.BundleType
import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.JAR_MIMETYPE import app.revanced.manager.util.JAR_MIMETYPE
@ -45,7 +30,7 @@ import app.revanced.manager.util.JAR_MIMETYPE
fun ImportPatchBundleDialog( fun ImportPatchBundleDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onRemoteSubmit: (String, Boolean) -> Unit, onRemoteSubmit: (String, Boolean) -> Unit,
onLocalSubmit: (Uri, Uri?) -> Unit onLocalSubmit: (Uri, Uri?) -> Unit,
) { ) {
var currentStep by rememberSaveable { mutableIntStateOf(0) } var currentStep by rememberSaveable { mutableIntStateOf(0) }
var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) } var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) }
@ -72,31 +57,32 @@ fun ImportPatchBundleDialog(
integrationsActivityLauncher.launch(APK_MIMETYPE) integrationsActivityLauncher.launch(APK_MIMETYPE)
} }
val steps = listOf<@Composable () -> Unit>( val steps =
{ listOf<@Composable () -> Unit>(
SelectBundleTypeStep(bundleType) { selectedType -> {
bundleType = selectedType SelectBundleTypeStep(bundleType) { selectedType ->
} bundleType = selectedType
}, }
{ },
ImportBundleStep( {
bundleType, ImportBundleStep(
patchBundle, bundleType,
integrations, patchBundle,
remoteUrl, integrations,
autoUpdate, remoteUrl,
{ launchPatchActivity() }, autoUpdate,
{ launchIntegrationsActivity() }, { launchPatchActivity() },
{ remoteUrl = it }, { launchIntegrationsActivity() },
{ autoUpdate = it } { remoteUrl = it },
) { autoUpdate = it },
} )
) },
)
val inputsAreValid by remember { val inputsAreValid by remember {
derivedStateOf { derivedStateOf {
(bundleType == BundleType.Local && patchBundle != null) || (bundleType == BundleType.Local && patchBundle != null) ||
(bundleType == BundleType.Remote && remoteUrl.isNotEmpty()) (bundleType == BundleType.Remote && remoteUrl.isNotEmpty())
} }
} }
@ -114,16 +100,17 @@ fun ImportPatchBundleDialog(
enabled = inputsAreValid, enabled = inputsAreValid,
onClick = { onClick = {
when (bundleType) { when (bundleType) {
BundleType.Local -> patchBundle?.let { BundleType.Local ->
onLocalSubmit( patchBundle?.let {
it, onLocalSubmit(
integrations it,
) integrations,
} )
}
BundleType.Remote -> onRemoteSubmit(remoteUrl, autoUpdate) BundleType.Remote -> onRemoteSubmit(remoteUrl, autoUpdate)
} }
} },
) { ) {
Text(stringResource(R.string.add)) Text(stringResource(R.string.add))
} }
@ -144,53 +131,55 @@ fun ImportPatchBundleDialog(
} }
} }
}, },
textHorizontalPadding = PaddingValues(0.dp) textHorizontalPadding = PaddingValues(0.dp),
) )
} }
@Composable @Composable
fun SelectBundleTypeStep( fun SelectBundleTypeStep(
bundleType: BundleType, bundleType: BundleType,
onBundleTypeSelected: (BundleType) -> Unit onBundleTypeSelected: (BundleType) -> Unit,
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(24.dp) verticalArrangement = Arrangement.spacedBy(24.dp),
) { ) {
Text( Text(
modifier = Modifier.padding(horizontal = 24.dp), modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.select_bundle_type_dialog_description) text = stringResource(R.string.select_bundle_type_dialog_description),
) )
Column { Column {
ListItem( ListItem(
modifier = Modifier.clickable( modifier =
role = Role.RadioButton, Modifier.clickable(
onClick = { onBundleTypeSelected(BundleType.Remote) } role = Role.RadioButton,
), onClick = { onBundleTypeSelected(BundleType.Remote) },
),
headlineContent = { Text(stringResource(R.string.enter_url)) }, headlineContent = { Text(stringResource(R.string.enter_url)) },
overlineContent = { Text(stringResource(R.string.recommended)) }, overlineContent = { Text(stringResource(R.string.recommended)) },
supportingContent = { Text(stringResource(R.string.remote_bundle_description)) }, supportingContent = { Text(stringResource(R.string.remote_bundle_description)) },
leadingContent = { leadingContent = {
RadioButton( HapticRadioButton(
selected = bundleType == BundleType.Remote, selected = bundleType == BundleType.Remote,
onClick = null onClick = null,
) )
} },
) )
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
ListItem( ListItem(
modifier = Modifier.clickable( modifier =
role = Role.RadioButton, Modifier.clickable(
onClick = { onBundleTypeSelected(BundleType.Local) } role = Role.RadioButton,
), onClick = { onBundleTypeSelected(BundleType.Local) },
),
headlineContent = { Text(stringResource(R.string.select_from_storage)) }, headlineContent = { Text(stringResource(R.string.select_from_storage)) },
supportingContent = { Text(stringResource(R.string.local_bundle_description)) }, supportingContent = { Text(stringResource(R.string.local_bundle_description)) },
overlineContent = { }, overlineContent = { },
leadingContent = { leadingContent = {
RadioButton( HapticRadioButton(
selected = bundleType == BundleType.Local, selected = bundleType == BundleType.Local,
onClick = null onClick = null,
) )
} },
) )
} }
} }
@ -207,67 +196,92 @@ fun ImportBundleStep(
launchPatchActivity: () -> Unit, launchPatchActivity: () -> Unit,
launchIntegrationsActivity: () -> Unit, launchIntegrationsActivity: () -> Unit,
onRemoteUrlChange: (String) -> Unit, onRemoteUrlChange: (String) -> Unit,
onAutoUpdateChange: (Boolean) -> Unit onAutoUpdateChange: (Boolean) -> Unit,
) { ) {
Column { Column {
when (bundleType) { when (bundleType) {
BundleType.Local -> { BundleType.Local -> {
Column( Column(
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 8.dp),
) { ) {
ListItem( ListItem(
headlineContent = { headlineContent = {
Text(stringResource(R.string.patch_bundle_field)) Text(stringResource(R.string.patch_bundle_field))
}, },
supportingContent = { Text(stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set)) }, supportingContent = {
Text(
stringResource(
if (patchBundle !=
null
) {
R.string.file_field_set
} else {
R.string.file_field_not_set
},
),
)
},
trailingContent = { trailingContent = {
IconButton(onClick = launchPatchActivity) { IconButton(onClick = launchPatchActivity) {
Icon(imageVector = Icons.Default.Topic, contentDescription = null) Icon(imageVector = Icons.Default.Topic, contentDescription = null)
} }
}, },
modifier = Modifier.clickable { launchPatchActivity() } modifier = Modifier.clickable { launchPatchActivity() },
) )
ListItem( ListItem(
headlineContent = { headlineContent = {
Text(stringResource(R.string.integrations_field)) Text(stringResource(R.string.integrations_field))
}, },
supportingContent = { Text(stringResource(if (integrations != null) R.string.file_field_set else R.string.file_field_not_set)) }, supportingContent = {
Text(
stringResource(
if (integrations !=
null
) {
R.string.file_field_set
} else {
R.string.file_field_not_set
},
),
)
},
trailingContent = { trailingContent = {
IconButton(onClick = launchIntegrationsActivity) { IconButton(onClick = launchIntegrationsActivity) {
Icon(imageVector = Icons.Default.Topic, contentDescription = null) Icon(imageVector = Icons.Default.Topic, contentDescription = null)
} }
}, },
modifier = Modifier.clickable { launchIntegrationsActivity() } modifier = Modifier.clickable { launchIntegrationsActivity() },
) )
} }
} }
BundleType.Remote -> { BundleType.Remote -> {
Column( Column(
modifier = Modifier.padding(TextHorizontalPadding) modifier = Modifier.padding(TextHorizontalPadding),
) { ) {
OutlinedTextField( OutlinedTextField(
value = remoteUrl, value = remoteUrl,
onValueChange = onRemoteUrlChange, onValueChange = onRemoteUrlChange,
label = { Text(stringResource(R.string.bundle_url)) } label = { Text(stringResource(R.string.bundle_url)) },
) )
} }
Column( Column(
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 8.dp),
) { ) {
ListItem( ListItem(
modifier = Modifier.clickable( modifier =
role = Role.Checkbox, Modifier.clickable(
onClick = { onAutoUpdateChange(!autoUpdate) } role = Role.Checkbox,
), onClick = { onAutoUpdateChange(!autoUpdate) },
),
headlineContent = { Text(stringResource(R.string.auto_update)) }, headlineContent = { Text(stringResource(R.string.auto_update)) },
leadingContent = { leadingContent = {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
Checkbox( HapticCheckbox(
checked = autoUpdate, checked = autoUpdate,
onCheckedChange = { onCheckedChange = {
onAutoUpdateChange(!autoUpdate) onAutoUpdateChange(!autoUpdate)
} },
) )
} }
}, },
@ -276,4 +290,4 @@ fun ImportBundleStep(
} }
} }
} }
} }

View file

@ -1,29 +1,24 @@
package app.revanced.manager.ui.component.haptics package app.revanced.manager.ui.component.haptics
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.RadioButtonColors import androidx.compose.material3.RadioButtonColors
import androidx.compose.material3.RadioButtonDefaults import androidx.compose.material3.RadioButtonDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
@Composable @Composable
fun HapticRadioButton ( fun HapticRadioButton(
selected: Boolean, selected: Boolean,
onClick: (() -> Unit)?, onClick: (() -> Unit)?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
colors: RadioButtonColors = RadioButtonDefaults.colors(), colors: RadioButtonColors = RadioButtonDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) { ) {
val selectedState = remember { mutableStateOf(selected) } val selectedState = remember { mutableStateOf(selected) }
@ -42,6 +37,6 @@ fun HapticRadioButton (
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
colors = colors, colors = colors,
interactionSource = interactionSource interactionSource = interactionSource,
) )
} }

View file

@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -16,11 +15,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.ui.component.haptics.HapticRadioButton
@Composable @Composable
fun InstallPickerDialog( fun InstallPickerDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onConfirm: (InstallType) -> Unit onConfirm: (InstallType) -> Unit,
) { ) {
var selectedInstallType by rememberSaveable { mutableStateOf(InstallType.DEFAULT) } var selectedInstallType by rememberSaveable { mutableStateOf(InstallType.DEFAULT) }
@ -36,7 +36,7 @@ fun InstallPickerDialog(
onClick = { onClick = {
onConfirm(selectedInstallType) onConfirm(selectedInstallType)
onDismiss() onDismiss()
} },
) { ) {
Text(stringResource(R.string.install_app)) Text(stringResource(R.string.install_app))
} }
@ -44,19 +44,19 @@ fun InstallPickerDialog(
title = { Text(stringResource(R.string.select_install_type)) }, title = { Text(stringResource(R.string.select_install_type)) },
text = { text = {
Column { Column {
InstallType.values().forEach { InstallType.entries.forEach {
ListItem( ListItem(
modifier = Modifier.clickable { selectedInstallType = it }, modifier = Modifier.clickable { selectedInstallType = it },
leadingContent = { leadingContent = {
RadioButton( HapticRadioButton(
selected = selectedInstallType == it, selected = selectedInstallType == it,
onClick = null onClick = null,
) )
}, },
headlineContent = { Text(stringResource(it.stringResource)) } headlineContent = { Text(stringResource(it.stringResource)) },
) )
} }
} }
} },
) )
} }

View file

@ -31,13 +31,11 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -65,6 +63,8 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.FloatInputDialog import app.revanced.manager.ui.component.FloatInputDialog
import app.revanced.manager.ui.component.IntInputDialog import app.revanced.manager.ui.component.IntInputDialog
import app.revanced.manager.ui.component.LongInputDialog import app.revanced.manager.ui.component.LongInputDialog
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.component.haptics.HapticSwitch import app.revanced.manager.ui.component.haptics.HapticSwitch
import app.revanced.manager.util.isScrollingUp import app.revanced.manager.util.isScrollingUp
import app.revanced.manager.util.mutableStateSetOf import app.revanced.manager.util.mutableStateSetOf
@ -443,7 +443,7 @@ private class PresetOptionEditor<T : Any>(
headlineContent = { Text(title) }, headlineContent = { Text(title) },
supportingContent = value?.toString()?.let { { Text(it) } }, supportingContent = value?.toString()?.let { { Text(it) } },
leadingContent = { leadingContent = {
RadioButton( HapticRadioButton(
selected = selectedPreset == presetKey, selected = selectedPreset == presetKey,
onClick = { selectedPreset = presetKey }, onClick = { selectedPreset = presetKey },
) )
@ -601,7 +601,7 @@ private class ListOptionEditor<T : Serializable>(
floatingActionButton = { floatingActionButton = {
if (deleteMode) return@Scaffold if (deleteMode) return@Scaffold
ExtendedFloatingActionButton( HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.add)) }, text = { Text(stringResource(R.string.add)) },
icon = { Icon(Icons.Outlined.Add, null) }, icon = { Icon(Icons.Outlined.Add, null) },
expanded = lazyListState.isScrollingUp, expanded = lazyListState.isScrollingUp,

View file

@ -35,6 +35,7 @@ import app.revanced.manager.ui.component.AppTopBar
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.SearchView
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
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_SUPPORTED
@ -50,15 +51,16 @@ import kotlinx.coroutines.launch
fun PatchesSelectorScreen( fun PatchesSelectorScreen(
onSave: (PatchSelection?, Options) -> Unit, onSave: (PatchSelection?, Options) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: PatchesSelectorViewModel vm: PatchesSelectorViewModel,
) { ) {
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList()) val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
val pagerState = rememberPagerState( val pagerState =
initialPage = 0, rememberPagerState(
initialPageOffsetFraction = 0f initialPage = 0,
) { initialPageOffsetFraction = 0f,
bundles.size ) {
} bundles.size
}
val composableScope = rememberCoroutineScope() val composableScope = rememberCoroutineScope()
var search: String? by rememberSaveable { var search: String? by rememberSaveable {
mutableStateOf(null) mutableStateOf(null)
@ -74,30 +76,30 @@ fun PatchesSelectorScreen(
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { onDismissRequest = {
showBottomSheet = false showBottomSheet = false
} },
) { ) {
Column( Column(
modifier = Modifier.padding(horizontal = 24.dp) modifier = Modifier.padding(horizontal = 24.dp),
) { ) {
Text( Text(
text = stringResource(R.string.patch_selector_sheet_filter_title), text = stringResource(R.string.patch_selector_sheet_filter_title),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(bottom = 16.dp) modifier = Modifier.padding(bottom = 16.dp),
) )
Text( Text(
text = stringResource(R.string.patch_selector_sheet_filter_compat_title), text = stringResource(R.string.patch_selector_sheet_filter_compat_title),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
) )
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp) horizontalArrangement = Arrangement.spacedBy(5.dp),
) { ) {
FilterChip( FilterChip(
selected = vm.filter and SHOW_SUPPORTED != 0, selected = vm.filter and SHOW_SUPPORTED != 0,
onClick = { vm.toggleFlag(SHOW_SUPPORTED) }, onClick = { vm.toggleFlag(SHOW_SUPPORTED) },
label = { Text(stringResource(R.string.supported)) } label = { Text(stringResource(R.string.supported)) },
) )
FilterChip( FilterChip(
@ -116,12 +118,13 @@ fun PatchesSelectorScreen(
} }
} }
if (vm.compatibleVersions.isNotEmpty()) if (vm.compatibleVersions.isNotEmpty()) {
UnsupportedDialog( UnsupportedDialog(
appVersion = vm.appVersion, appVersion = vm.appVersion,
supportedVersions = vm.compatibleVersions, supportedVersions = vm.compatibleVersions,
onDismissRequest = vm::dismissDialogs onDismissRequest = vm::dismissDialogs,
) )
}
vm.optionsDialog?.let { (bundle, patch) -> vm.optionsDialog?.let { (bundle, patch) ->
OptionsDialog( OptionsDialog(
@ -129,7 +132,7 @@ fun PatchesSelectorScreen(
patch = patch, patch = patch,
values = vm.getOptions(bundle, patch), values = vm.getOptions(bundle, patch),
reset = { vm.resetOptions(bundle, patch) }, reset = { vm.resetOptions(bundle, patch) },
set = { key, value -> vm.setOption(bundle, patch, key, value) } set = { key, value -> vm.setOption(bundle, patch, key, value) },
) )
} }
@ -142,7 +145,7 @@ fun PatchesSelectorScreen(
vm.pendingUniversalPatchAction?.let { vm.pendingUniversalPatchAction?.let {
UniversalPatchWarningDialog( UniversalPatchWarningDialog(
onCancel = vm::dismissUniversalPatchWarning, onCancel = vm::dismissUniversalPatchWarning,
onConfirm = vm::confirmUniversalPatchWarning onConfirm = vm::confirmUniversalPatchWarning,
) )
} }
@ -151,7 +154,7 @@ fun PatchesSelectorScreen(
patches: List<PatchInfo>, patches: List<PatchInfo>,
filterFlag: Int, filterFlag: Int,
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() && (vm.filter and filterFlag) != 0 || vm.filter == 0) {
header?.let { header?.let {
@ -162,17 +165,19 @@ fun PatchesSelectorScreen(
items( items(
items = patches, items = patches,
key = { it.name } key = { it.name },
) { patch -> ) { patch ->
PatchItem( PatchItem(
patch = patch, patch = patch,
onOptionsDialog = { onOptionsDialog = {
vm.optionsDialog = uid to patch vm.optionsDialog = uid to patch
}, },
selected = supported && vm.isSelected( selected =
uid, supported &&
patch vm.isSelected(
), uid,
patch,
),
onToggle = { onToggle = {
if (vm.selectionWarningEnabled) { if (vm.selectionWarningEnabled) {
showSelectionWarning = true showSelectionWarning = true
@ -182,7 +187,7 @@ fun PatchesSelectorScreen(
vm.togglePatch(uid, patch) vm.togglePatch(uid, patch)
} }
}, },
supported = supported supported = supported,
) )
} }
} }
@ -193,28 +198,29 @@ fun PatchesSelectorScreen(
query = query, query = query,
onQueryChange = { search = it }, onQueryChange = { search = it },
onActiveChange = { if (!it) search = null }, onActiveChange = { if (!it) search = null },
placeholder = { Text(stringResource(R.string.search_patches)) } placeholder = { Text(stringResource(R.string.search_patches)) },
) { ) {
val bundle = bundles[pagerState.currentPage] val bundle = bundles[pagerState.currentPage]
LazyColumnWithScrollbar( LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize(),
) { ) {
fun List<PatchInfo>.searched() = filter { fun List<PatchInfo>.searched() =
it.name.contains(query, true) filter {
} it.name.contains(query, true)
}
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.supported.searched(), patches = bundle.supported.searched(),
filterFlag = SHOW_SUPPORTED, filterFlag = SHOW_SUPPORTED,
supported = true supported = true,
) )
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.universal.searched(), patches = bundle.universal.searched(),
filterFlag = SHOW_UNIVERSAL, filterFlag = SHOW_UNIVERSAL,
supported = true supported = true,
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.universal_patches), title = stringResource(R.string.universal_patches),
@ -226,11 +232,11 @@ fun PatchesSelectorScreen(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.unsupported.searched(), patches = bundle.unsupported.searched(),
filterFlag = SHOW_UNSUPPORTED, filterFlag = SHOW_UNSUPPORTED,
supported = true supported = true,
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.unsupported_patches), title = stringResource(R.string.unsupported_patches),
onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) } onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) },
) )
} }
} }
@ -252,11 +258,11 @@ fun PatchesSelectorScreen(
IconButton( IconButton(
onClick = { onClick = {
search = "" search = ""
} },
) { ) {
Icon(Icons.Outlined.Search, stringResource(R.string.search)) Icon(Icons.Outlined.Search, stringResource(R.string.search))
} }
} },
) )
}, },
floatingActionButton = { floatingActionButton = {
@ -269,19 +275,19 @@ fun PatchesSelectorScreen(
onClick = { onClick = {
// TODO: only allow this if all required options have been set. // TODO: only allow this if all required options have been set.
onSave(vm.getCustomSelection(), vm.getOptions()) onSave(vm.getCustomSelection(), vm.getOptions())
} },
) )
} },
) { paddingValues -> ) { paddingValues ->
Column( Column(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues),
) { ) {
if (bundles.size > 1) { if (bundles.size > 1) {
ScrollableTabRow( ScrollableTabRow(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp),
) { ) {
bundles.forEachIndexed { index, bundle -> bundles.forEachIndexed { index, bundle ->
HapticTab( HapticTab(
@ -289,13 +295,13 @@ fun PatchesSelectorScreen(
onClick = { onClick = {
composableScope.launch { composableScope.launch {
pagerState.animateScrollToPage( pagerState.animateScrollToPage(
index index,
) )
} }
}, },
text = { Text(bundle.name) }, text = { Text(bundle.name) },
selectedContentColor = MaterialTheme.colorScheme.primary, selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
} }
@ -311,19 +317,19 @@ fun PatchesSelectorScreen(
LazyColumnWithScrollbar( LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = patchLazyListStates[index] state = patchLazyListStates[index],
) { ) {
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.supported, patches = bundle.supported,
filterFlag = SHOW_SUPPORTED, filterFlag = SHOW_SUPPORTED,
supported = true supported = true,
) )
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.universal, patches = bundle.universal,
filterFlag = SHOW_UNIVERSAL, filterFlag = SHOW_UNIVERSAL,
supported = true supported = true,
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.universal_patches), title = stringResource(R.string.universal_patches),
@ -333,15 +339,15 @@ fun PatchesSelectorScreen(
uid = bundle.uid, uid = bundle.uid,
patches = bundle.unsupported, patches = bundle.unsupported,
filterFlag = SHOW_UNSUPPORTED, filterFlag = SHOW_UNSUPPORTED,
supported = vm.allowIncompatiblePatches supported = vm.allowIncompatiblePatches,
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.unsupported_patches), title = stringResource(R.string.unsupported_patches),
onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) } onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) },
) )
} }
} }
} },
) )
} }
} }
@ -359,7 +365,7 @@ fun SelectionWarningDialog(onDismiss: () -> Unit) {
@Composable @Composable
fun UniversalPatchWarningDialog( fun UniversalPatchWarningDialog(
onCancel: () -> Unit, onCancel: () -> Unit,
onConfirm: () -> Unit onConfirm: () -> Unit,
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = onCancel, onDismissRequest = onCancel,
@ -379,12 +385,12 @@ fun UniversalPatchWarningDialog(
title = { title = {
Text( Text(
text = stringResource(R.string.warning), text = stringResource(R.string.warning),
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center) style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
) )
}, },
text = { text = {
Text(stringResource(R.string.universal_patch_warning_description)) Text(stringResource(R.string.universal_patch_warning_description))
} },
) )
} }
@ -394,17 +400,18 @@ fun PatchItem(
onOptionsDialog: () -> Unit, onOptionsDialog: () -> Unit,
selected: Boolean, selected: Boolean,
onToggle: () -> Unit, onToggle: () -> Unit,
supported: Boolean = true supported: Boolean = true,
) = ListItem ( ) = ListItem(
modifier = Modifier modifier =
.let { if (!supported) it.alpha(0.5f) else it } Modifier
.clickable(enabled = supported, onClick = onToggle) .let { if (!supported) it.alpha(0.5f) else it }
.fillMaxSize(), .clickable(enabled = supported, onClick = onToggle)
.fillMaxSize(),
leadingContent = { leadingContent = {
HapticCheckbox( HapticCheckbox(
checked = selected, checked = selected,
onCheckedChange = { onToggle() }, onCheckedChange = { onToggle() },
enabled = supported enabled = supported,
) )
}, },
headlineContent = { Text(patch.name) }, headlineContent = { Text(patch.name) },
@ -421,26 +428,27 @@ fun PatchItem(
@Composable @Composable
fun ListHeader( fun ListHeader(
title: String, title: String,
onHelpClick: (() -> Unit)? = null onHelpClick: (() -> Unit)? = null,
) { ) {
ListItem( ListItem(
headlineContent = { headlineContent = {
Text( Text(
text = title, text = title,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelLarge style = MaterialTheme.typography.labelLarge,
) )
}, },
trailingContent = onHelpClick?.let { trailingContent =
{ onHelpClick?.let {
IconButton(onClick = it) { {
Icon( IconButton(onClick = it) {
Icons.AutoMirrored.Outlined.HelpOutline, Icon(
stringResource(R.string.help) Icons.AutoMirrored.Outlined.HelpOutline,
) stringResource(R.string.help),
)
}
} }
} },
}
) )
} }
@ -448,7 +456,7 @@ fun ListHeader(
fun UnsupportedDialog( fun UnsupportedDialog(
appVersion: String, appVersion: String,
supportedVersions: List<String>, supportedVersions: List<String>,
onDismissRequest: () -> Unit onDismissRequest: () -> Unit,
) = AlertDialog( ) = AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
confirmButton = { confirmButton = {
@ -462,10 +470,10 @@ fun UnsupportedDialog(
stringResource( stringResource(
R.string.app_not_supported, R.string.app_not_supported,
appVersion, appVersion,
supportedVersions.joinToString(", ") supportedVersions.joinToString(", "),
) ),
) )
} },
) )
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -478,10 +486,11 @@ fun OptionsDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) = Dialog( ) = Dialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
properties = DialogProperties( properties =
usePlatformDefaultWidth = false, DialogProperties(
dismissOnBackPress = true usePlatformDefaultWidth = false,
) dismissOnBackPress = true,
),
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
@ -492,12 +501,12 @@ fun OptionsDialog(
IconButton(onClick = reset) { IconButton(onClick = reset) {
Icon(Icons.Outlined.Restore, stringResource(R.string.reset)) Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
} }
} },
) )
} },
) { paddingValues -> ) { paddingValues ->
LazyColumnWithScrollbar( LazyColumnWithScrollbar(
modifier = Modifier.padding(paddingValues) modifier = Modifier.padding(paddingValues),
) { ) {
if (patch.options == null) return@LazyColumnWithScrollbar if (patch.options == null) return@LazyColumnWithScrollbar
@ -511,4 +520,4 @@ fun OptionsDialog(
} }
} }
} }
} }

View file

@ -9,7 +9,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight 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.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -28,6 +27,7 @@ import app.revanced.manager.R
import app.revanced.manager.ui.component.AppInfo import app.revanced.manager.ui.component.AppInfo
import app.revanced.manager.ui.component.AppTopBar 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.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.destination.SelectedAppInfoDestination import app.revanced.manager.ui.destination.SelectedAppInfoDestination
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
@ -48,7 +48,7 @@ import org.koin.core.parameter.parametersOf
fun SelectedAppInfoScreen( fun SelectedAppInfoScreen(
onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit, onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: SelectedAppInfoViewModel vm: SelectedAppInfoViewModel,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -82,67 +82,71 @@ fun SelectedAppInfoScreen(
AnimatedNavHost(controller = navController) { destination -> AnimatedNavHost(controller = navController) { destination ->
when (destination) { when (destination) {
is SelectedAppInfoDestination.Main -> SelectedAppInfoScreen( is SelectedAppInfoDestination.Main ->
onPatchClick = patchClick@{ SelectedAppInfoScreen(
if (selectedPatchCount == 0) { onPatchClick = patchClick@{
context.toast(context.getString(R.string.no_patches_selected)) if (selectedPatchCount == 0) {
context.toast(context.getString(R.string.no_patches_selected))
return@patchClick return@patchClick
} }
onPatchClick( onPatchClick(
vm.selectedApp,
patches,
vm.getOptionsFiltered(bundles)
)
},
onPatchSelectorClick = {
navController.navigate(
SelectedAppInfoDestination.PatchesSelector(
vm.selectedApp, vm.selectedApp,
vm.getCustomPatches( patches,
bundles, vm.getOptionsFiltered(bundles),
allowIncompatiblePatches )
},
onPatchSelectorClick = {
navController.navigate(
SelectedAppInfoDestination.PatchesSelector(
vm.selectedApp,
vm.getCustomPatches(
bundles,
allowIncompatiblePatches,
),
vm.options,
), ),
vm.options
) )
) },
}, onVersionSelectorClick = {
onVersionSelectorClick = { navController.navigate(SelectedAppInfoDestination.VersionSelector)
navController.navigate(SelectedAppInfoDestination.VersionSelector) },
}, onBackClick = onBackClick,
onBackClick = onBackClick, availablePatchCount = availablePatchCount,
availablePatchCount = availablePatchCount, selectedPatchCount = selectedPatchCount,
selectedPatchCount = selectedPatchCount, packageName = packageName,
packageName = packageName, version = version,
version = version, packageInfo = vm.selectedAppInfo,
packageInfo = vm.selectedAppInfo, )
)
is SelectedAppInfoDestination.VersionSelector -> VersionSelectorScreen( is SelectedAppInfoDestination.VersionSelector ->
onBackClick = navController::pop, VersionSelectorScreen(
onAppClick = { onBackClick = navController::pop,
vm.selectedApp = it onAppClick = {
navController.pop() vm.selectedApp = it
}, navController.pop()
viewModel = koinViewModel { parametersOf(packageName) } },
) viewModel = koinViewModel { parametersOf(packageName) },
)
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen( is SelectedAppInfoDestination.PatchesSelector ->
onSave = { patches, options -> PatchesSelectorScreen(
vm.updateConfiguration(patches, options, bundles) onSave = { patches, options ->
navController.pop() vm.updateConfiguration(patches, options, bundles)
}, navController.pop()
onBackClick = navController::pop, },
vm = koinViewModel { onBackClick = navController::pop,
parametersOf( vm =
PatchesSelectorViewModel.Params( koinViewModel {
destination.app, parametersOf(
destination.currentSelection, PatchesSelectorViewModel.Params(
destination.options, destination.app,
) destination.currentSelection,
) destination.options,
} ),
) )
},
)
} }
} }
} }
@ -164,21 +168,22 @@ private fun SelectedAppInfoScreen(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.app_info), title = stringResource(R.string.app_info),
onBackClick = onBackClick onBackClick = onBackClick,
) )
}, },
floatingActionButton = { floatingActionButton = {
ExtendedFloatingActionButton( HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.patch)) }, text = { Text(stringResource(R.string.patch)) },
icon = { Icon(Icons.Default.AutoFixHigh, null) }, icon = { Icon(Icons.Default.AutoFixHigh, null) },
onClick = onPatchClick onClick = onPatchClick,
) )
} },
) { paddingValues -> ) { paddingValues ->
ColumnWithScrollbar( ColumnWithScrollbar(
modifier = Modifier modifier =
.fillMaxSize() Modifier
.padding(paddingValues) .fillMaxSize()
.padding(paddingValues),
) { ) {
AppInfo(packageInfo, placeholderLabel = packageName) { AppInfo(packageInfo, placeholderLabel = packageName) {
Text( Text(
@ -191,39 +196,44 @@ private fun SelectedAppInfoScreen(
PageItem( PageItem(
R.string.patch_selector_item, R.string.patch_selector_item,
stringResource(R.string.patch_selector_item_description, selectedPatchCount), stringResource(R.string.patch_selector_item_description, selectedPatchCount),
onPatchSelectorClick onPatchSelectorClick,
) )
PageItem( PageItem(
R.string.version_selector_item, R.string.version_selector_item,
stringResource(R.string.version_selector_item_description, version), stringResource(R.string.version_selector_item_description, version),
onVersionSelectorClick onVersionSelectorClick,
) )
} }
} }
} }
@Composable @Composable
private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) { private fun PageItem(
@StringRes title: Int,
description: String,
onClick: () -> Unit,
) {
ListItem( ListItem(
modifier = Modifier modifier =
.clickable(onClick = onClick) Modifier
.padding(start = 8.dp), .clickable(onClick = onClick)
.padding(start = 8.dp),
headlineContent = { headlineContent = {
Text( Text(
stringResource(title), stringResource(title),
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge,
) )
}, },
supportingContent = { supportingContent = {
Text( Text(
description, description,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
) )
}, },
trailingContent = { trailingContent = {
Icon(Icons.AutoMirrored.Outlined.ArrowRight, null) Icon(Icons.AutoMirrored.Outlined.ArrowRight, null)
} },
) )
} }

View file

@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -20,6 +19,7 @@ import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar 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.GroupHeader import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.ui.component.settings.BooleanItem import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.DownloadsViewModel import app.revanced.manager.ui.viewmodel.DownloadsViewModel
@ -29,7 +29,7 @@ import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun DownloadsSettingsScreen( fun DownloadsSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
viewModel: DownloadsViewModel = koinViewModel() viewModel: DownloadsViewModel = koinViewModel(),
) { ) {
val prefs = viewModel.prefs val prefs = viewModel.prefs
@ -46,14 +46,15 @@ fun DownloadsSettingsScreen(
Icon(Icons.Default.Delete, stringResource(R.string.delete)) Icon(Icons.Default.Delete, stringResource(R.string.delete))
} }
} }
} },
) )
} },
) { paddingValues -> ) { paddingValues ->
ColumnWithScrollbar( ColumnWithScrollbar(
modifier = Modifier modifier =
.fillMaxSize() Modifier
.padding(paddingValues) .fillMaxSize()
.padding(paddingValues),
) { ) {
BooleanItem( BooleanItem(
preference = prefs.preferSplits, preference = prefs.preferSplits,
@ -69,16 +70,21 @@ fun DownloadsSettingsScreen(
SettingsListItem( SettingsListItem(
modifier = Modifier.clickable { viewModel.toggleItem(app) }, modifier = Modifier.clickable { viewModel.toggleItem(app) },
headlineContent = app.packageName, headlineContent = app.packageName,
leadingContent = (@Composable { leadingContent =
Checkbox( {
checked = selected, (
onCheckedChange = { viewModel.toggleItem(app) } @Composable {
) HapticCheckbox(
}).takeIf { viewModel.selection.isNotEmpty() }, checked = selected,
onCheckedChange = { viewModel.toggleItem(app) },
)
}
).takeIf { viewModel.selection.isNotEmpty() }
},
supportingContent = app.version, supportingContent = app.version,
tonalElevation = if (selected) 8.dp else 0.dp tonalElevation = if (selected) 8.dp else 0.dp,
) )
} }
} }
} }
} }