From 2b0784865a2fd75e75b13f225f9ccbe6811f7683 Mon Sep 17 00:00:00 2001 From: Brosssh <44944126+Brosssh@users.noreply.github.com> Date: Wed, 14 May 2025 19:55:09 +0200 Subject: [PATCH] feat(Compose): Add confirmation dialog on multiple operations (#2529) --- .../java/app/revanced/manager/MainActivity.kt | 2 +- .../manager/ui/component/ConfirmDialog.kt | 41 ++++++++++ .../manager/ui/component/bundle/BundleItem.kt | 23 ++++-- .../manager/ui/screen/DashboardScreen.kt | 19 ++++- .../manager/ui/screen/PatcherScreen.kt | 75 +++++++++++-------- .../settings/DownloadsSettingsScreen.kt | 15 +++- app/src/main/res/values/strings.xml | 10 +++ 7 files changed, 145 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/ui/component/ConfirmDialog.kt diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 4135bc22..c4e7a7e1 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -164,7 +164,7 @@ private fun ReVancedManager(vm: MainViewModel) { } } }, - vm = koinViewModel { parametersOf(it.getComplexArg()) } + viewModel = koinViewModel { parametersOf(it.getComplexArg()) } ) } diff --git a/app/src/main/java/app/revanced/manager/ui/component/ConfirmDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/ConfirmDialog.kt new file mode 100644 index 00000000..fe5e0d9e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/ConfirmDialog.kt @@ -0,0 +1,41 @@ +package app.revanced.manager.ui.component + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import app.revanced.manager.R + +@Composable +fun ConfirmDialog( + onDismiss: () -> Unit, + onConfirm: () -> Unit, + title: String, + description: String, + icon: ImageVector +) { + AlertDialog( + onDismissRequest = onDismiss, + dismissButton = { + TextButton(onDismiss) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + onConfirm() + onDismiss() + } + ) { + Text(stringResource(R.string.confirm)) + } + }, + title = { Text(title) }, + icon = { Icon(icon, null) }, + text = { Text(description) } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt index 6f3ae914..d1644df4 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.Warning import androidx.compose.material3.Icon @@ -26,8 +27,9 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.domain.bundles.PatchBundleSource -import app.revanced.manager.ui.component.haptics.HapticCheckbox import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState +import app.revanced.manager.ui.component.ConfirmDialog +import app.revanced.manager.ui.component.haptics.HapticCheckbox import kotlinx.coroutines.flow.map @OptIn(ExperimentalFoundationApi::class) @@ -42,6 +44,7 @@ fun BundleItem( toggleSelection: (Boolean) -> Unit, ) { var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) } + var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) } val state by bundle.state.collectAsStateWithLifecycle() val version by remember(bundle) { @@ -52,15 +55,25 @@ fun BundleItem( if (viewBundleDialogPage) { BundleInformationDialog( onDismissRequest = { viewBundleDialogPage = false }, - onDeleteRequest = { - viewBundleDialogPage = false - onDelete() - }, + onDeleteRequest = { showDeleteConfirmationDialog = true }, bundle = bundle, onUpdate = onUpdate, ) } + if (showDeleteConfirmationDialog) { + ConfirmDialog( + onDismiss = { showDeleteConfirmationDialog = false }, + onConfirm = { + onDelete() + viewBundleDialogPage = false + }, + title = stringResource(R.string.bundle_delete_single_dialog_title), + description = stringResource(R.string.bundle_delete_single_dialog_description, name), + icon = Icons.Outlined.Delete + ) + } + ListItem( modifier = Modifier .height(64.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 bfd02e1c..0d14cf9e 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 @@ -21,6 +21,7 @@ import androidx.compose.material.icons.filled.BatteryAlert import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.outlined.Apps import androidx.compose.material.icons.outlined.BugReport +import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.Refresh @@ -62,6 +63,7 @@ import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.component.AvailableUpdateDialog import app.revanced.manager.ui.component.NotificationCard +import app.revanced.manager.ui.component.ConfirmDialog import app.revanced.manager.ui.component.bundle.BundleTopBar import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton @@ -154,6 +156,20 @@ fun DashboardScreen( } ) + var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) } + if (showDeleteConfirmationDialog) { + ConfirmDialog( + onDismiss = { showDeleteConfirmationDialog = false }, + onConfirm = { + vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) } + vm.cancelSourceSelection() + }, + title = stringResource(R.string.bundle_delete_multiple_dialog_title), + description = stringResource(R.string.bundle_delete_multiple_dialog_description), + icon = Icons.Outlined.Delete + ) + } + Scaffold( topBar = { if (bundlesSelectable) { @@ -169,8 +185,7 @@ fun DashboardScreen( actions = { IconButton( onClick = { - vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) } - vm.cancelSourceSelection() + showDeleteConfirmationDialog = true } ) { Icon( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index da69619f..065f919f 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.OpenInNew +import androidx.compose.material.icons.outlined.Cancel import androidx.compose.material.icons.outlined.FileDownload import androidx.compose.material.icons.outlined.PostAdd import androidx.compose.material.icons.outlined.Save @@ -45,6 +46,7 @@ import app.revanced.manager.R import app.revanced.manager.data.room.apps.installed.InstallType import app.revanced.manager.ui.component.AppScaffold import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.ConfirmDialog import app.revanced.manager.ui.component.InstallerStatusDialog import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.patcher.InstallPickerDialog @@ -58,25 +60,23 @@ import app.revanced.manager.util.EventEffect @Composable fun PatcherScreen( onBackClick: () -> Unit, - vm: PatcherViewModel + viewModel: PatcherViewModel ) { - fun leaveScreen() { - vm.onBack() - onBackClick() - } - BackHandler(onBack = ::leaveScreen) val context = LocalContext.current val exportApkLauncher = - rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export) + rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export) - val patcherSucceeded by vm.patcherSucceeded.observeAsState(null) - val canInstall by remember { derivedStateOf { patcherSucceeded == true && (vm.installedPackageName != null || !vm.isInstalling) } } + val patcherSucceeded by viewModel.patcherSucceeded.observeAsState(null) + val canInstall by remember { derivedStateOf { patcherSucceeded == true && (viewModel.installedPackageName != null || !viewModel.isInstalling) } } var showInstallPicker by rememberSaveable { mutableStateOf(false) } + var showDismissConfirmationDialog by rememberSaveable { mutableStateOf(false) } + + BackHandler(onBack = { showDismissConfirmationDialog = true }) val steps by remember { derivedStateOf { - vm.steps.groupBy { it.category } + viewModel.steps.groupBy { it.category } } } @@ -93,34 +93,47 @@ fun PatcherScreen( if (showInstallPicker) InstallPickerDialog( onDismiss = { showInstallPicker = false }, - onConfirm = vm::install + onConfirm = viewModel::install ) - vm.packageInstallerStatus?.let { - InstallerStatusDialog(it, vm, vm::dismissPackageInstallerDialog) + if (showDismissConfirmationDialog) { + ConfirmDialog( + onDismiss = { showDismissConfirmationDialog = false }, + onConfirm = { + viewModel.onBack() + onBackClick() + }, + title = stringResource(R.string.patcher_stop_confirm_title), + description = stringResource(R.string.patcher_stop_confirm_description), + icon = Icons.Outlined.Cancel + ) + } + + viewModel.packageInstallerStatus?.let { + InstallerStatusDialog(it, viewModel, viewModel::dismissPackageInstallerDialog) } val activityLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult(), - onResult = vm::handleActivityResult + onResult = viewModel::handleActivityResult ) - EventEffect(flow = vm.launchActivityFlow) { intent -> + EventEffect(flow = viewModel.launchActivityFlow) { intent -> activityLauncher.launch(intent) } - vm.activityPromptDialog?.let { title -> + viewModel.activityPromptDialog?.let { title -> AlertDialog( - onDismissRequest = vm::rejectInteraction, + onDismissRequest = viewModel::rejectInteraction, confirmButton = { TextButton( - onClick = vm::allowInteraction + onClick = viewModel::allowInteraction ) { Text(stringResource(R.string.continue_)) } }, dismissButton = { TextButton( - onClick = vm::rejectInteraction + onClick = viewModel::rejectInteraction ) { Text(stringResource(R.string.cancel)) } @@ -137,20 +150,20 @@ fun PatcherScreen( AppTopBar( title = stringResource(R.string.patcher), scrollBehavior = scrollBehavior, - onBackClick = ::leaveScreen + onBackClick = { showDismissConfirmationDialog = true } ) }, bottomBar = { BottomAppBar( actions = { IconButton( - onClick = { exportApkLauncher.launch("${vm.packageName}_${vm.version}_revanced_patched.apk") }, + onClick = { exportApkLauncher.launch("${viewModel.packageName}_${viewModel.version}_revanced_patched.apk") }, enabled = patcherSucceeded == true ) { Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk)) } IconButton( - onClick = { vm.exportLogs(context) }, + onClick = { viewModel.exportLogs(context) }, enabled = patcherSucceeded != null ) { Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs)) @@ -161,11 +174,11 @@ fun PatcherScreen( HapticExtendedFloatingActionButton( text = { Text( - stringResource(if (vm.installedPackageName == null) R.string.install_app else R.string.open_app) + stringResource(if (viewModel.installedPackageName == null) R.string.install_app else R.string.open_app) ) }, icon = { - vm.installedPackageName?.let { + viewModel.installedPackageName?.let { Icon( Icons.AutoMirrored.Outlined.OpenInNew, stringResource(R.string.open_app) @@ -176,10 +189,10 @@ fun PatcherScreen( ) }, onClick = { - if (vm.installedPackageName == null) - if (vm.isDeviceRooted()) showInstallPicker = true - else vm.install(InstallType.DEFAULT) - else vm.open() + if (viewModel.installedPackageName == null) + if (viewModel.isDeviceRooted()) showInstallPicker = true + else viewModel.install(InstallType.DEFAULT) + else viewModel.open() } ) } @@ -193,7 +206,7 @@ fun PatcherScreen( .fillMaxSize() ) { LinearProgressIndicator( - progress = { vm.progress }, + progress = { viewModel.progress }, modifier = Modifier.fillMaxWidth() ) @@ -209,8 +222,8 @@ fun PatcherScreen( Steps( category = category, steps = steps, - stepCount = if (category == StepCategory.PATCHING) vm.patchesProgress else null, - stepProgressProvider = vm + stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null, + stepProgressProvider = viewModel ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt index 96247a03..3d57ee50 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -43,6 +44,7 @@ import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.ExceptionViewerDialog import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.LazyColumnWithScrollbar +import app.revanced.manager.ui.component.ConfirmDialog import app.revanced.manager.ui.component.haptics.HapticCheckbox import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.viewmodel.DownloadsViewModel @@ -59,6 +61,17 @@ fun DownloadsSettingsScreen( val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList()) val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) } + + if (showDeleteConfirmationDialog) { + ConfirmDialog( + onDismiss = { showDeleteConfirmationDialog = false }, + onConfirm = { viewModel.deleteApps() }, + title = stringResource(R.string.downloader_plugin_delete_apps_title), + description = stringResource(R.string.downloader_plugin_delete_apps_description), + icon = Icons.Outlined.Delete + ) + } Scaffold( topBar = { @@ -68,7 +81,7 @@ fun DownloadsSettingsScreen( onBackClick = onBackClick, actions = { if (viewModel.appSelection.isNotEmpty()) { - IconButton(onClick = { viewModel.deleteApps() }) { + IconButton(onClick = { showDeleteConfirmationDialog = true }) { Icon(Icons.Default.Delete, stringResource(R.string.delete)) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2fb78470..fc28a580 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -143,6 +143,8 @@ Trust plugin? Revoke trust? Package name: %1$s\nSignature (SHA-256): %2$s + Delete selected apps + Are you sure you want to delete the selected apps? No downloaded apps found Search apps… @@ -301,6 +303,8 @@ Write patched APK file Sign patched APK file Patching in progress… + Stop patcher + Are you sure you want to stop the patching process? Execute patches Execute %s Failed to execute %s @@ -336,6 +340,11 @@ View patches Any version Any package + Delete bundle + Delete bundles + Are you sure you want to delete the bundle \"%s\"? + Are you sure you want to delete the selected bundles? + About ReVanced Manager ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance. @@ -425,4 +434,5 @@ Shows a popup notification whenever there is a new update available on launch. Failed to import keystore Export + Confirm