From 1bf004ddeee0dc889dbf49f73ff78a612f1e8e4a Mon Sep 17 00:00:00 2001 From: Ushie Date: Wed, 3 Jul 2024 03:48:04 +0300 Subject: [PATCH] feat: Progressive AlertDialog for adding bundles Closes #1992 --- .../ui/component/AlertDialogExtended.kt | 6 +- .../ui/component/bundle/ImportBundleDialog.kt | 335 ++++++++++++------ .../bundle/ImportBundleTypeSelectorDialog.kt | 95 ----- .../manager/ui/screen/DashboardScreen.kt | 34 +- app/src/main/res/values/strings.xml | 13 +- 5 files changed, 240 insertions(+), 243 deletions(-) delete mode 100644 app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleTypeSelectorDialog.kt diff --git a/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt b/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt index 0cfd3453..cceb189f 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt @@ -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( @@ -147,4 +147,6 @@ private fun ContentStyle( content() } } -} \ No newline at end of file +} + +val TextHorizontalPadding = PaddingValues(horizontal = 24.dp) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt index 1402cf82..f75f3e52 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt @@ -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(null) } var integrations by rememberSaveable { mutableStateOf(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) - ) - }, - actions = { - TextButton( - enabled = inputsAreValid, - onClick = { - when (bundleType) { - BundleType.Local -> onLocalSubmit( - name, - patchBundle!!, - 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 + AlertDialogExtended( + onDismissRequest = onDismiss, + title = { + Text(stringResource(if (currentStep == 0) R.string.select else R.string.add_patch_bundle)) + }, + text = { + steps[currentStep]() + }, + confirmButton = { + if (currentStep == steps.size - 1) { + TextButton( + enabled = inputsAreValid, + onClick = { + when (bundleType) { + BundleType.Local -> patchBundle?.let { + onLocalSubmit( + "BundleName", + it, + integrations + ) + } - 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 - ) { - Icon( - imageVector = Icons.Default.Topic, - contentDescription = null - ) + BundleType.Remote -> onRemoteSubmit("BundleName", remoteUrl, autoUpdate) } - }, - modifier = Modifier.clickable { - launchPatchActivity() } - ) - - 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 - ) { - Icon( - imageVector = Icons.Default.Topic, - contentDescription = null - ) - } - }, - modifier = Modifier.clickable { - launchIntegrationsActivity() - } - ) + ) { + Text(stringResource(R.string.add)) + } + } else { + TextButton(onClick = { currentStep++ }) { + Text(stringResource(R.string.next)) + } } + }, + 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) + ) +} + +@Composable +fun SelectBundleTypeStep( + bundleType: BundleType, + onBundleTypeSelected: (BundleType) -> Unit +) { + 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 = { 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) + } + ) + } + }, + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleTypeSelectorDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleTypeSelectorDialog.kt deleted file mode 100644 index b3f32ad9..00000000 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleTypeSelectorDialog.kt +++ /dev/null @@ -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) - ) -} diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt index 9d147d34..10c306bb 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt @@ -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 } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9e57f97b..a4610470 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,8 +18,8 @@ Bundle patches Patch bundle Integrations - Provided - Not provided + Selected + Not selected Not set @@ -331,12 +331,17 @@ Download update? No contributors found Select - Select bundle type - Select the type that is right for you. + Add new bundle + Add a new bundle from a URL or storage Import local files from your storage, does not automatically update Import remote files from a URL, can automatically update Recommended Show Debugging About device + Enter URL + Next + Add patch bundle + Bundle URL + Auto update