feat(Compose): Add confirmation dialog on multiple operations (#2529)

This commit is contained in:
Brosssh 2025-05-14 19:55:09 +02:00 committed by oSumAtrIX
parent d7c0913277
commit 2b0784865a
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
7 changed files with 145 additions and 40 deletions

View File

@ -164,7 +164,7 @@ private fun ReVancedManager(vm: MainViewModel) {
} }
} }
}, },
vm = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) } viewModel = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
) )
} }

View File

@ -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) }
)
}

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons 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.ErrorOutline
import androidx.compose.material.icons.outlined.Warning import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -26,8 +27,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource 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.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 import kotlinx.coroutines.flow.map
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -42,6 +44,7 @@ fun BundleItem(
toggleSelection: (Boolean) -> Unit, toggleSelection: (Boolean) -> Unit,
) { ) {
var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) } var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) }
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
val state by bundle.state.collectAsStateWithLifecycle() val state by bundle.state.collectAsStateWithLifecycle()
val version by remember(bundle) { val version by remember(bundle) {
@ -52,15 +55,25 @@ fun BundleItem(
if (viewBundleDialogPage) { if (viewBundleDialogPage) {
BundleInformationDialog( BundleInformationDialog(
onDismissRequest = { viewBundleDialogPage = false }, onDismissRequest = { viewBundleDialogPage = false },
onDeleteRequest = { onDeleteRequest = { showDeleteConfirmationDialog = true },
viewBundleDialogPage = false
onDelete()
},
bundle = bundle, bundle = bundle,
onUpdate = onUpdate, 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( ListItem(
modifier = Modifier modifier = Modifier
.height(64.dp) .height(64.dp)

View File

@ -21,6 +21,7 @@ import androidx.compose.material.icons.filled.BatteryAlert
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Apps import androidx.compose.material.icons.outlined.Apps
import androidx.compose.material.icons.outlined.BugReport 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.DeleteOutline
import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Refresh 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.AutoUpdatesDialog
import app.revanced.manager.ui.component.AvailableUpdateDialog import app.revanced.manager.ui.component.AvailableUpdateDialog
import app.revanced.manager.ui.component.NotificationCard 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.BundleTopBar
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton 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( Scaffold(
topBar = { topBar = {
if (bundlesSelectable) { if (bundlesSelectable) {
@ -169,8 +185,7 @@ fun DashboardScreen(
actions = { actions = {
IconButton( IconButton(
onClick = { onClick = {
vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) } showDeleteConfirmationDialog = true
vm.cancelSourceSelection()
} }
) { ) {
Icon( Icon(

View File

@ -17,6 +17,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.OpenInNew 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.FileDownload
import androidx.compose.material.icons.outlined.PostAdd import androidx.compose.material.icons.outlined.PostAdd
import androidx.compose.material.icons.outlined.Save 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.data.room.apps.installed.InstallType
import app.revanced.manager.ui.component.AppScaffold import app.revanced.manager.ui.component.AppScaffold
import app.revanced.manager.ui.component.AppTopBar 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.InstallerStatusDialog
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.patcher.InstallPickerDialog import app.revanced.manager.ui.component.patcher.InstallPickerDialog
@ -58,25 +60,23 @@ import app.revanced.manager.util.EventEffect
@Composable @Composable
fun PatcherScreen( fun PatcherScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: PatcherViewModel viewModel: PatcherViewModel
) { ) {
fun leaveScreen() {
vm.onBack()
onBackClick()
}
BackHandler(onBack = ::leaveScreen)
val context = LocalContext.current val context = LocalContext.current
val exportApkLauncher = val exportApkLauncher =
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export) rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), viewModel::export)
val patcherSucceeded by vm.patcherSucceeded.observeAsState(null) val patcherSucceeded by viewModel.patcherSucceeded.observeAsState(null)
val canInstall by remember { derivedStateOf { patcherSucceeded == true && (vm.installedPackageName != null || !vm.isInstalling) } } val canInstall by remember { derivedStateOf { patcherSucceeded == true && (viewModel.installedPackageName != null || !viewModel.isInstalling) } }
var showInstallPicker by rememberSaveable { mutableStateOf(false) } var showInstallPicker by rememberSaveable { mutableStateOf(false) }
var showDismissConfirmationDialog by rememberSaveable { mutableStateOf(false) }
BackHandler(onBack = { showDismissConfirmationDialog = true })
val steps by remember { val steps by remember {
derivedStateOf { derivedStateOf {
vm.steps.groupBy { it.category } viewModel.steps.groupBy { it.category }
} }
} }
@ -93,34 +93,47 @@ fun PatcherScreen(
if (showInstallPicker) if (showInstallPicker)
InstallPickerDialog( InstallPickerDialog(
onDismiss = { showInstallPicker = false }, onDismiss = { showInstallPicker = false },
onConfirm = vm::install onConfirm = viewModel::install
) )
vm.packageInstallerStatus?.let { if (showDismissConfirmationDialog) {
InstallerStatusDialog(it, vm, vm::dismissPackageInstallerDialog) 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( val activityLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(), contract = ActivityResultContracts.StartActivityForResult(),
onResult = vm::handleActivityResult onResult = viewModel::handleActivityResult
) )
EventEffect(flow = vm.launchActivityFlow) { intent -> EventEffect(flow = viewModel.launchActivityFlow) { intent ->
activityLauncher.launch(intent) activityLauncher.launch(intent)
} }
vm.activityPromptDialog?.let { title -> viewModel.activityPromptDialog?.let { title ->
AlertDialog( AlertDialog(
onDismissRequest = vm::rejectInteraction, onDismissRequest = viewModel::rejectInteraction,
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = vm::allowInteraction onClick = viewModel::allowInteraction
) { ) {
Text(stringResource(R.string.continue_)) Text(stringResource(R.string.continue_))
} }
}, },
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = vm::rejectInteraction onClick = viewModel::rejectInteraction
) { ) {
Text(stringResource(R.string.cancel)) Text(stringResource(R.string.cancel))
} }
@ -137,20 +150,20 @@ fun PatcherScreen(
AppTopBar( AppTopBar(
title = stringResource(R.string.patcher), title = stringResource(R.string.patcher),
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
onBackClick = ::leaveScreen onBackClick = { showDismissConfirmationDialog = true }
) )
}, },
bottomBar = { bottomBar = {
BottomAppBar( BottomAppBar(
actions = { actions = {
IconButton( IconButton(
onClick = { exportApkLauncher.launch("${vm.packageName}_${vm.version}_revanced_patched.apk") }, onClick = { exportApkLauncher.launch("${viewModel.packageName}_${viewModel.version}_revanced_patched.apk") },
enabled = patcherSucceeded == true enabled = patcherSucceeded == true
) { ) {
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk)) Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
} }
IconButton( IconButton(
onClick = { vm.exportLogs(context) }, onClick = { viewModel.exportLogs(context) },
enabled = patcherSucceeded != null enabled = patcherSucceeded != null
) { ) {
Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs)) Icon(Icons.Outlined.PostAdd, stringResource(id = R.string.save_logs))
@ -161,11 +174,11 @@ fun PatcherScreen(
HapticExtendedFloatingActionButton( HapticExtendedFloatingActionButton(
text = { text = {
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 = { icon = {
vm.installedPackageName?.let { viewModel.installedPackageName?.let {
Icon( Icon(
Icons.AutoMirrored.Outlined.OpenInNew, Icons.AutoMirrored.Outlined.OpenInNew,
stringResource(R.string.open_app) stringResource(R.string.open_app)
@ -176,10 +189,10 @@ fun PatcherScreen(
) )
}, },
onClick = { onClick = {
if (vm.installedPackageName == null) if (viewModel.installedPackageName == null)
if (vm.isDeviceRooted()) showInstallPicker = true if (viewModel.isDeviceRooted()) showInstallPicker = true
else vm.install(InstallType.DEFAULT) else viewModel.install(InstallType.DEFAULT)
else vm.open() else viewModel.open()
} }
) )
} }
@ -193,7 +206,7 @@ fun PatcherScreen(
.fillMaxSize() .fillMaxSize()
) { ) {
LinearProgressIndicator( LinearProgressIndicator(
progress = { vm.progress }, progress = { viewModel.progress },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
@ -209,8 +222,8 @@ fun PatcherScreen(
Steps( Steps(
category = category, category = category,
steps = steps, steps = steps,
stepCount = if (category == StepCategory.PATCHING) vm.patchesProgress else null, stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null,
stepProgressProvider = vm stepProgressProvider = viewModel
) )
} }
} }

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
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.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon 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.ExceptionViewerDialog
import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.LazyColumnWithScrollbar 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.haptics.HapticCheckbox
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
@ -59,6 +61,17 @@ fun DownloadsSettingsScreen(
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList()) val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle() val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) 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( Scaffold(
topBar = { topBar = {
@ -68,7 +81,7 @@ fun DownloadsSettingsScreen(
onBackClick = onBackClick, onBackClick = onBackClick,
actions = { actions = {
if (viewModel.appSelection.isNotEmpty()) { if (viewModel.appSelection.isNotEmpty()) {
IconButton(onClick = { viewModel.deleteApps() }) { IconButton(onClick = { showDeleteConfirmationDialog = true }) {
Icon(Icons.Default.Delete, stringResource(R.string.delete)) Icon(Icons.Default.Delete, stringResource(R.string.delete))
} }
} }

View File

@ -143,6 +143,8 @@
<string name="downloader_plugin_trust_dialog_title">Trust plugin?</string> <string name="downloader_plugin_trust_dialog_title">Trust plugin?</string>
<string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string> <string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string>
<string name="downloader_plugin_trust_dialog_body">Package name: %1$s\nSignature (SHA-256): %2$s</string> <string name="downloader_plugin_trust_dialog_body">Package name: %1$s\nSignature (SHA-256): %2$s</string>
<string name="downloader_plugin_delete_apps_title">Delete selected apps</string>
<string name="downloader_plugin_delete_apps_description">Are you sure you want to delete the selected apps?</string>
<string name="downloader_settings_no_apps">No downloaded apps found</string> <string name="downloader_settings_no_apps">No downloaded apps found</string>
<string name="search_apps">Search apps…</string> <string name="search_apps">Search apps…</string>
@ -301,6 +303,8 @@
<string name="patcher_step_write_patched">Write patched APK file</string> <string name="patcher_step_write_patched">Write patched APK file</string>
<string name="patcher_step_sign_apk">Sign patched APK file</string> <string name="patcher_step_sign_apk">Sign patched APK file</string>
<string name="patcher_notification_message">Patching in progress…</string> <string name="patcher_notification_message">Patching in progress…</string>
<string name="patcher_stop_confirm_title">Stop patcher</string>
<string name="patcher_stop_confirm_description">Are you sure you want to stop the patching process?</string>
<string name="execute_patches">Execute patches</string> <string name="execute_patches">Execute patches</string>
<string name="executing_patch">Execute %s</string> <string name="executing_patch">Execute %s</string>
<string name="failed_to_execute_patch">Failed to execute %s</string> <string name="failed_to_execute_patch">Failed to execute %s</string>
@ -336,6 +340,11 @@
<string name="bundle_view_patches">View patches</string> <string name="bundle_view_patches">View patches</string>
<string name="bundle_view_patches_any_version">Any version</string> <string name="bundle_view_patches_any_version">Any version</string>
<string name="bundle_view_patches_any_package">Any package</string> <string name="bundle_view_patches_any_package">Any package</string>
<string name="bundle_delete_single_dialog_title">Delete bundle</string>
<string name="bundle_delete_multiple_dialog_title">Delete bundles</string>
<string name="bundle_delete_single_dialog_description">Are you sure you want to delete the bundle \"%s\"?</string>
<string name="bundle_delete_multiple_dialog_description">Are you sure you want to delete the selected bundles?</string>
<string name="about_revanced_manager">About ReVanced Manager</string> <string name="about_revanced_manager">About ReVanced Manager</string>
<string name="revanced_manager_description">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.</string> <string name="revanced_manager_description">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.</string>
@ -425,4 +434,5 @@
<string name="show_manager_update_dialog_on_launch_description">Shows a popup notification whenever there is a new update available on launch.</string> <string name="show_manager_update_dialog_on_launch_description">Shows a popup notification whenever there is a new update available on launch.</string>
<string name="failed_to_import_keystore">Failed to import keystore</string> <string name="failed_to_import_keystore">Failed to import keystore</string>
<string name="export">Export</string> <string name="export">Export</string>
<string name="confirm">Confirm</string>
</resources> </resources>