feat: Progressive AlertDialog for adding bundles

Closes #1992
This commit is contained in:
Ushie 2024-07-03 03:48:04 +03:00
parent 495100dea9
commit 1bf004ddee
No known key found for this signature in database
GPG key ID: B3AAD18842E34632
5 changed files with 240 additions and 243 deletions

View file

@ -44,7 +44,7 @@ fun AlertDialogExtended(
titleContentColor: Color = AlertDialogDefaults.titleContentColor,
textContentColor: Color = AlertDialogDefaults.textContentColor,
tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
textHorizontalPadding: PaddingValues = PaddingValues(horizontal = 24.dp)
textHorizontalPadding: PaddingValues = TextHorizontalPadding
) {
BasicAlertDialog(onDismissRequest = onDismissRequest) {
Surface(
@ -148,3 +148,5 @@ private fun ContentStyle(
}
}
}
val TextHorizontalPadding = PaddingValues(horizontal = 24.dp)

View file

@ -4,166 +4,269 @@ import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Topic
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
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.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import app.revanced.manager.R
import app.revanced.manager.ui.component.AlertDialogExtended
import app.revanced.manager.ui.component.TextHorizontalPadding
import app.revanced.manager.ui.model.BundleType
import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.JAR_MIMETYPE
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ImportBundleDialog(
onDismissRequest: () -> Unit,
fun ImportPatchBundleDialog(
onDismiss: () -> Unit,
onRemoteSubmit: (String, String, Boolean) -> Unit,
onLocalSubmit: (String, Uri, Uri?) -> Unit,
initialBundleType: BundleType
onLocalSubmit: (String, Uri, Uri?) -> Unit
) {
var name by rememberSaveable { mutableStateOf("") }
var remoteUrl by rememberSaveable { mutableStateOf("") }
var autoUpdate by rememberSaveable { mutableStateOf(true) }
var bundleType by rememberSaveable { mutableStateOf(initialBundleType) }
var currentStep by rememberSaveable { mutableIntStateOf(0) }
var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) }
var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) }
var integrations by rememberSaveable { mutableStateOf<Uri?>(null) }
val inputsAreValid by remember {
derivedStateOf {
name.isNotEmpty() && if (bundleType == BundleType.Local) patchBundle != null else remoteUrl.isNotEmpty()
}
}
var remoteUrl by rememberSaveable { mutableStateOf("") }
var autoUpdate by rememberSaveable { mutableStateOf(false) }
val patchActivityLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { patchBundle = it }
}
fun launchPatchActivity() {
patchActivityLauncher.launch(JAR_MIMETYPE)
}
val integrationsActivityLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { integrations = it }
}
fun launchIntegrationsActivity() {
integrationsActivityLauncher.launch(APK_MIMETYPE)
val steps = listOf<@Composable () -> Unit>(
{
SelectBundleTypeStep(bundleType) { selectedType ->
bundleType = selectedType
}
},
{
ImportBundleStep(
bundleType,
patchBundle,
integrations,
remoteUrl,
autoUpdate,
{ patchActivityLauncher.launch(JAR_MIMETYPE) },
{ integrationsActivityLauncher.launch(APK_MIMETYPE) },
{ remoteUrl = it },
{ autoUpdate = it }
)
}
)
val inputsAreValid by remember {
derivedStateOf {
(bundleType == BundleType.Local && patchBundle != null) ||
(bundleType == BundleType.Remote && remoteUrl.isNotEmpty())
}
}
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnBackPress = true
)
) {
Scaffold(
topBar = {
BundleTopBar(
title = stringResource(R.string.import_bundle),
onBackClick = onDismissRequest,
onBackIcon = {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.close)
)
AlertDialogExtended(
onDismissRequest = onDismiss,
title = {
Text(stringResource(if (currentStep == 0) R.string.select else R.string.add_patch_bundle))
},
actions = {
text = {
steps[currentStep]()
},
confirmButton = {
if (currentStep == steps.size - 1) {
TextButton(
enabled = inputsAreValid,
onClick = {
when (bundleType) {
BundleType.Local -> onLocalSubmit(
name,
patchBundle!!,
BundleType.Local -> patchBundle?.let {
onLocalSubmit(
"BundleName",
it,
integrations
)
BundleType.Remote -> onRemoteSubmit(name, remoteUrl, autoUpdate)
}
},
modifier = Modifier.padding(end = 16.dp)
) {
Text(stringResource(R.string.import_))
}
}
)
},
) { paddingValues ->
BaseBundleDialog(
modifier = Modifier.padding(paddingValues),
isDefault = false,
name = name,
onNameChange = { name = it },
remoteUrl = remoteUrl.takeUnless { bundleType == BundleType.Local },
onRemoteUrlChange = { remoteUrl = it },
patchCount = 0,
version = null,
autoUpdate = autoUpdate,
onAutoUpdateChange = { autoUpdate = it },
onPatchesClick = {},
onBundleTypeClick = {
bundleType = when (bundleType) {
BundleType.Local -> BundleType.Remote
BundleType.Remote -> BundleType.Local
}
},
) {
if (bundleType == BundleType.Remote) return@BaseBundleDialog
BundleListItem(
headlineText = stringResource(R.string.patch_bundle_field),
supportingText = stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set),
trailingContent = {
IconButton(
onClick = ::launchPatchActivity
BundleType.Remote -> onRemoteSubmit("BundleName", remoteUrl, autoUpdate)
}
}
) {
Icon(
imageVector = Icons.Default.Topic,
contentDescription = null
)
Text(stringResource(R.string.add))
}
} else {
TextButton(onClick = { currentStep++ }) {
Text(stringResource(R.string.next))
}
}
},
modifier = Modifier.clickable {
launchPatchActivity()
dismissButton = {
if (currentStep > 0) {
TextButton(onClick = { currentStep-- }) {
Text(stringResource(R.string.back))
}
} else {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
}
},
textHorizontalPadding = PaddingValues(0.dp)
)
}
BundleListItem(
headlineText = stringResource(R.string.integrations_field),
supportingText = stringResource(if (integrations != null) R.string.file_field_set else R.string.file_field_not_set),
trailingContent = {
IconButton(
onClick = ::launchIntegrationsActivity
@Composable
fun SelectBundleTypeStep(
bundleType: BundleType,
onBundleTypeSelected: (BundleType) -> Unit
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Icon(
imageVector = Icons.Default.Topic,
contentDescription = null
Text(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.select_bundle_type_dialog_description)
)
Column {
ListItem(
modifier = Modifier.clickable(
role = Role.RadioButton,
onClick = { onBundleTypeSelected(BundleType.Remote) }
),
headlineContent = { Text(stringResource(R.string.enter_url)) },
overlineContent = { Text(stringResource(R.string.recommended)) },
supportingContent = { Text(stringResource(R.string.remote_bundle_description)) },
leadingContent = {
RadioButton(
selected = bundleType == BundleType.Remote,
onClick = null
)
}
)
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
ListItem(
modifier = Modifier.clickable(
role = Role.RadioButton,
onClick = { onBundleTypeSelected(BundleType.Local) }
),
headlineContent = { Text(stringResource(R.string.select_from_storage)) },
supportingContent = { Text(stringResource(R.string.local_bundle_description)) },
overlineContent = { },
leadingContent = {
RadioButton(
selected = bundleType == BundleType.Local,
onClick = null
)
}
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ImportBundleStep(
bundleType: BundleType,
patchBundle: Uri?,
integrations: Uri?,
remoteUrl: String,
autoUpdate: Boolean,
launchPatchActivity: () -> Unit,
launchIntegrationsActivity: () -> Unit,
onRemoteUrlChange: (String) -> Unit,
onAutoUpdateChange: (Boolean) -> Unit
) {
Column {
when (bundleType) {
BundleType.Local -> {
Column(
modifier = Modifier.padding(horizontal = 8.dp)
) {
ListItem(
headlineContent = {
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)) },
trailingContent = {
IconButton(onClick = launchPatchActivity) {
Icon(imageVector = Icons.Default.Topic, contentDescription = null)
}
},
modifier = Modifier.clickable { launchPatchActivity() }
)
ListItem(
headlineContent = {
Text(stringResource(R.string.integrations_field))
},
supportingContent = { Text(stringResource(if (integrations != null) R.string.file_field_set else R.string.file_field_not_set)) },
trailingContent = {
IconButton(onClick = launchIntegrationsActivity) {
Icon(imageVector = Icons.Default.Topic, contentDescription = null)
}
},
modifier = Modifier.clickable { launchIntegrationsActivity() }
)
}
}
BundleType.Remote -> {
Column(
modifier = Modifier.padding(TextHorizontalPadding)
) {
OutlinedTextField(
value = remoteUrl,
onValueChange = onRemoteUrlChange,
label = { Text(stringResource(R.string.bundle_url)) }
)
}
Column(
modifier = Modifier.padding(horizontal = 8.dp)
) {
ListItem(
modifier = Modifier.clickable(
role = Role.Checkbox,
onClick = { onAutoUpdateChange(!autoUpdate) }
),
headlineContent = { Text(stringResource(R.string.auto_update)) },
leadingContent = {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
Checkbox(
checked = autoUpdate,
onCheckedChange = {
onAutoUpdateChange(!autoUpdate)
}
)
}
},
modifier = Modifier.clickable {
launchIntegrationsActivity()
}
)
}
}
}
}
}

View file

@ -1,95 +0,0 @@
package app.revanced.manager.ui.component.bundle
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ListItem
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.AlertDialogExtended
import app.revanced.manager.ui.model.BundleType
@Composable
fun ImportBundleTypeSelectorDialog(
onDismiss: () -> Unit,
onConfirm: (BundleType) -> Unit,
) {
var bundleType: BundleType by rememberSaveable { mutableStateOf(BundleType.Remote) }
AlertDialogExtended(
onDismissRequest = onDismiss,
confirmButton = {
TextButton(
onClick = { onConfirm(bundleType) }
) {
Text(stringResource(R.string.select))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
},
title = {
Text(stringResource(R.string.select_bundle_type_dialog_title))
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Text(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(R.string.select_bundle_type_dialog_description)
)
Column {
ListItem(
modifier = Modifier.clickable(
role = Role.RadioButton,
onClick = { bundleType = BundleType.Remote }
),
headlineContent = { Text(stringResource(R.string.remote)) },
overlineContent = { Text(stringResource(R.string.recommended)) },
supportingContent = { Text(stringResource(R.string.remote_bundle_description)) },
leadingContent = {
RadioButton(
selected = bundleType == BundleType.Remote,
onClick = null
)
}
)
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
ListItem(
modifier = Modifier.clickable(
role = Role.RadioButton,
onClick = { bundleType = BundleType.Local }
),
headlineContent = { Text(stringResource(R.string.local)) },
supportingContent = { Text(stringResource(R.string.local_bundle_description)) },
overlineContent = { }, // we're using this parameter to force the 3-line ListItem state
leadingContent = {
RadioButton(
selected = bundleType == BundleType.Local,
onClick = null
)
}
)
}
}
},
textHorizontalPadding = PaddingValues(0.dp)
)
}

View file

@ -53,9 +53,7 @@ import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.component.NotificationCard
import app.revanced.manager.ui.component.bundle.BundleItem
import app.revanced.manager.ui.component.bundle.BundleTopBar
import app.revanced.manager.ui.component.bundle.ImportBundleDialog
import app.revanced.manager.ui.component.bundle.ImportBundleTypeSelectorDialog
import app.revanced.manager.ui.model.BundleType
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
import app.revanced.manager.ui.viewmodel.DashboardViewModel
import app.revanced.manager.util.toast
import kotlinx.coroutines.launch
@ -94,33 +92,17 @@ fun DashboardScreen(
val firstLaunch by vm.prefs.firstLaunch.getAsState()
if (firstLaunch) AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
var selectedBundleType: BundleType? by rememberSaveable { mutableStateOf(null) }
selectedBundleType?.let {
fun dismiss() {
selectedBundleType = null
}
ImportBundleDialog(
onDismissRequest = ::dismiss,
var showAddBundleDialog by rememberSaveable { mutableStateOf(false) }
if (showAddBundleDialog) {
ImportPatchBundleDialog(
onDismiss = { showAddBundleDialog = false },
onLocalSubmit = { name, patches, integrations ->
dismiss()
showAddBundleDialog = false
vm.createLocalSource(name, patches, integrations)
},
onRemoteSubmit = { name, url, autoUpdate ->
dismiss()
showAddBundleDialog = false
vm.createRemoteSource(name, url, autoUpdate)
},
initialBundleType = it
)
}
var showBundleTypeSelectorDialog by rememberSaveable { mutableStateOf(false) }
if (showBundleTypeSelectorDialog) {
ImportBundleTypeSelectorDialog(
onDismiss = { showBundleTypeSelectorDialog = false },
onConfirm = {
selectedBundleType = it
showBundleTypeSelectorDialog = false
}
)
}
@ -194,7 +176,7 @@ fun DashboardScreen(
}
DashboardPage.BUNDLES.ordinal -> {
showBundleTypeSelectorDialog = true
showAddBundleDialog = true
}
}
}

View file

@ -18,8 +18,8 @@
<string name="bundle_patches">Bundle patches</string>
<string name="patch_bundle_field">Patch bundle</string>
<string name="integrations_field">Integrations</string>
<string name="file_field_set">Provided</string>
<string name="file_field_not_set">Not provided</string>
<string name="file_field_set">Selected</string>
<string name="file_field_not_set">Not selected</string>
<string name="field_not_set">Not set</string>
@ -331,12 +331,17 @@
<string name="download_update_confirmation">Download update?</string>
<string name="no_contributors_found">No contributors found</string>
<string name="select">Select</string>
<string name="select_bundle_type_dialog_title">Select bundle type</string>
<string name="select_bundle_type_dialog_description">Select the type that is right for you.</string>
<string name="select_bundle_type_dialog_title">Add new bundle</string>
<string name="select_bundle_type_dialog_description">Add a new bundle from a URL or storage</string>
<string name="local_bundle_description">Import local files from your storage, does not automatically update</string>
<string name="remote_bundle_description">Import remote files from a URL, can automatically update</string>
<string name="recommended">Recommended</string>
<string name="show">Show</string>
<string name="debugging">Debugging</string>
<string name="about_device">About device</string>
<string name="enter_url">Enter URL</string>
<string name="next">Next</string>
<string name="add_patch_bundle">Add patch bundle</string>
<string name="bundle_url">Bundle URL</string>
<string name="auto_update">Auto update</string>
</resources>