feat: Add confirmation dialog to "Reset" options (#2576)

This commit is contained in:
brosssh
2025-06-10 16:38:05 +02:00
committed by GitHub
parent aebad0b0e2
commit 0992e63c28
10 changed files with 314 additions and 58 deletions

View File

@ -27,10 +27,10 @@ abstract class OptionDao {
abstract suspend fun createOptionGroup(group: OptionGroup)
@Query("DELETE FROM option_groups WHERE patch_bundle = :uid")
abstract suspend fun clearForPatchBundle(uid: Int)
abstract suspend fun resetOptionsForPatchBundle(uid: Int)
@Query("DELETE FROM option_groups WHERE package_name = :packageName")
abstract suspend fun clearForPackage(packageName: String)
abstract suspend fun resetOptionsForPackage(packageName: String)
@Query("DELETE FROM option_groups")
abstract suspend fun reset()

View File

@ -5,6 +5,7 @@ import androidx.room.Insert
import androidx.room.MapColumn
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
@Dao
abstract class SelectionDao {
@ -34,11 +35,14 @@ abstract class SelectionDao {
@Insert
abstract suspend fun createSelection(selection: PatchSelection)
@Query("SELECT package_name FROM patch_selections")
abstract fun getPackagesWithSelection(): Flow<List<String>>
@Query("DELETE FROM patch_selections WHERE patch_bundle = :uid")
abstract suspend fun clearForPatchBundle(uid: Int)
abstract suspend fun resetForPatchBundle(uid: Int)
@Query("DELETE FROM patch_selections WHERE package_name = :packageName")
abstract suspend fun clearForPackage(packageName: String)
abstract suspend fun resetForPackage(packageName: String)
@Query("DELETE FROM patch_selections")
abstract suspend fun reset()

View File

@ -76,7 +76,7 @@ class PatchOptionsRepository(db: AppDatabase) {
fun getPackagesWithSavedOptions() =
dao.getPackagesWithOptions().map(Iterable<String>::toSet).distinctUntilChanged()
suspend fun clearOptionsForPackage(packageName: String) = dao.clearForPackage(packageName)
suspend fun clearOptionsForPatchBundle(uid: Int) = dao.clearForPatchBundle(uid)
suspend fun resetOptionsForPackage(packageName: String) = dao.resetOptionsForPackage(packageName)
suspend fun resetOptionsForPatchBundle(uid: Int) = dao.resetOptionsForPatchBundle(uid)
suspend fun reset() = dao.reset()
}

View File

@ -3,6 +3,8 @@ package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.selection.PatchSelection
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
class PatchSelectionRepository(db: AppDatabase) {
private val dao = db.selectionDao()
@ -25,8 +27,15 @@ class PatchSelectionRepository(db: AppDatabase) {
)
})
suspend fun clearSelection(packageName: String) {
dao.clearForPackage(packageName)
fun getPackagesWithSavedSelection() =
dao.getPackagesWithSelection().map(Iterable<String>::toSet).distinctUntilChanged()
suspend fun resetSelectionForPackage(packageName: String) {
dao.resetForPackage(packageName)
}
suspend fun resetSelectionForPatchBundle(uid: Int) {
dao.resetForPatchBundle(uid)
}
suspend fun reset() = dao.reset()
@ -34,7 +43,7 @@ class PatchSelectionRepository(db: AppDatabase) {
suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid)
suspend fun import(bundleUid: Int, selection: SerializedSelection) {
dao.clearForPatchBundle(bundleUid)
dao.resetForPatchBundle(bundleUid)
dao.updateSelections(selection.entries.associate { (packageName, patches) ->
getOrCreateSelection(bundleUid, packageName) to patches.toSet()
})

View File

@ -13,11 +13,14 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import kotlinx.coroutines.flow.map
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -54,6 +57,10 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
}
bundles.forEach {
val name by it.nameState
val version by remember(it) {
it.propsFlow().map { props -> props?.version }
}.collectAsStateWithLifecycle(null)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
@ -65,7 +72,7 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
}
) {
Text(
name,
"$name $version",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)

View File

@ -1,6 +1,17 @@
package app.revanced.manager.ui.component.settings
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
@ -10,6 +21,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.material3.ListItem
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun SettingsListItem(
@ -67,4 +82,48 @@ fun SettingsListItem(
colors = colors,
tonalElevation = tonalElevation,
shadowElevation = shadowElevation
)
)
@Composable
fun ExpandableSettingListItem(
headlineContent: String,
supportingContent: String,
expandableContent: @Composable () -> Unit
) {
var expanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxWidth()
.animateContentSize()
) {
SettingsListItem(
modifier = Modifier
.clickable{ expanded = !expanded },
headlineContent = headlineContent,
supportingContent = supportingContent,
trailingContent = {
Icon(
imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = null
)
}
)
AnimatedVisibility(visible = expanded) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp, start = 16.dp, end = 16.dp)
.animateContentSize(
animationSpec = tween(
durationMillis = 500,
easing = FastOutSlowInEasing
)
)
) {
expandableContent()
}
}
}
}

View File

@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Key
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@ -28,6 +29,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@ -42,11 +44,14 @@ import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.ConfirmDialog
import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.PasswordField
import app.revanced.manager.ui.component.bundle.BundleSelector
import app.revanced.manager.ui.component.settings.ExpandableSettingListItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.ImportExportViewModel
import app.revanced.manager.ui.viewmodel.ResetDialogState
import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.launch
@ -59,6 +64,8 @@ fun ImportExportSettingsScreen(
vm: ImportExportViewModel = koinViewModel()
) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var selectorDialog by rememberSaveable { mutableStateOf<(@Composable () -> Unit)?>(null) }
val importKeystoreLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
@ -70,6 +77,7 @@ fun ImportExportSettingsScreen(
}
val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
val packagesWithSelections by vm.packagesWithSelection.collectAsStateWithLifecycle(initialValue = emptySet())
val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
vm.selectionAction?.let { action ->
@ -107,6 +115,20 @@ fun ImportExportSettingsScreen(
)
}
vm.resetDialogState?.let {
with(vm.resetDialogState!!) {
ConfirmDialog(
onDismiss = { vm.resetDialogState = null },
onConfirm = onConfirm,
title = stringResource(titleResId),
description = dialogOptionName?.let {
stringResource(descriptionResId, it)
} ?: stringResource(descriptionResId),
icon = Icons.Outlined.WarningAmber
)
}
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
@ -124,28 +146,7 @@ fun ImportExportSettingsScreen(
.fillMaxSize()
.padding(paddingValues)
) {
var showPackageSelector by rememberSaveable {
mutableStateOf(false)
}
var showBundleSelector by rememberSaveable {
mutableStateOf(false)
}
if (showPackageSelector) {
PackageSelector(packages = packagesWithOptions) { selected ->
selected?.let(vm::resetOptionsForPackage)
showPackageSelector = false
}
}
if (showBundleSelector) {
BundleSelector(bundles = patchBundles) { bundle ->
bundle?.let(vm::clearOptionsForBundle)
showBundleSelector = false
}
}
selectorDialog?.invoke()
GroupHeader(stringResource(R.string.import_))
GroupItem(
@ -181,32 +182,126 @@ fun ImportExportSettingsScreen(
GroupHeader(stringResource(R.string.reset))
GroupItem(
onClick = vm::regenerateKeystore,
onClick = {
vm.resetDialogState = ResetDialogState.Keystore {
vm.regenerateKeystore()
}
},
headline = R.string.regenerate_keystore,
description = R.string.regenerate_keystore_description
)
GroupItem(
onClick = vm::resetSelection, // TODO: allow resetting selection for specific bundle or package name.
headline = R.string.reset_patch_selection,
description = R.string.reset_patch_selection_description
ExpandableSettingListItem(
headlineContent = stringResource(R.string.reset_patch_selection),
supportingContent = stringResource(R.string.reset_patch_selection_description),
expandableContent = {
GroupItem(
onClick = {
vm.resetDialogState = ResetDialogState.PatchSelectionAll {
vm.resetSelection()
}
},
headline = R.string.patch_selection_reset_all,
description = R.string.patch_selection_reset_all_description
)
GroupItem(
onClick = {
selectorDialog = {
PackageSelector(packages = packagesWithSelections) { packageName ->
packageName?.also {
vm.resetDialogState =
ResetDialogState.PatchSelectionPackage(packageName) {
vm.resetSelectionForPackage(packageName)
}
}
selectorDialog = null
}
}
},
headline = R.string.patch_selection_reset_package,
description = R.string.patch_selection_reset_package_description
)
if (patchBundles.isNotEmpty()) {
GroupItem(
onClick = {
selectorDialog = {
BundleSelector(bundles = patchBundles) { bundle ->
bundle?.also {
coroutineScope.launch {
vm.resetDialogState =
ResetDialogState.PatchSelectionBundle(bundle.getName()) {
vm.resetSelectionForPatchBundle(bundle)
}
}
}
selectorDialog = null
}
}
},
headline = R.string.patch_selection_reset_bundle,
description = R.string.patch_selection_reset_bundle_description
)
}
}
)
GroupItem(
onClick = vm::resetOptions, // TODO: patch options import/export.
headline = R.string.patch_options_reset_all,
description = R.string.patch_options_reset_all_description,
ExpandableSettingListItem(
headlineContent = stringResource(R.string.reset_patch_options),
supportingContent = stringResource(R.string.reset_patch_options_description),
expandableContent = {
GroupItem(
onClick = {
vm.resetDialogState = ResetDialogState.PatchOptionsAll {
vm.resetOptions()
}
}, // TODO: patch options import/export.
headline = R.string.patch_options_reset_all,
description = R.string.patch_options_reset_all_description,
)
GroupItem(
onClick = {
selectorDialog = {
PackageSelector(packages = packagesWithOptions) { packageName ->
packageName?.also {
vm.resetDialogState =
ResetDialogState.PatchOptionPackage(packageName) {
vm.resetOptionsForPackage(packageName)
}
}
selectorDialog = null
}
}
},
headline = R.string.patch_options_reset_package,
description = R.string.patch_options_reset_package_description
)
if (patchBundles.isNotEmpty()) {
GroupItem(
onClick = {
selectorDialog = {
BundleSelector(bundles = patchBundles) { bundle ->
bundle?.also {
coroutineScope.launch {
vm.resetDialogState =
ResetDialogState.PatchOptionBundle(bundle.getName()) {
vm.resetOptionsForBundle(bundle)
}
}
}
selectorDialog = null
}
}
},
headline = R.string.patch_options_reset_bundle,
description = R.string.patch_options_reset_bundle_description,
)
}
}
)
GroupItem(
onClick = { showPackageSelector = true },
headline = R.string.patch_options_reset_package,
description = R.string.patch_options_reset_package_description
)
if (patchBundles.size > 1) {
GroupItem(
onClick = { showBundleSelector = true },
headline = R.string.patch_options_reset_bundle,
description = R.string.patch_options_reset_bundle_description,
)
}
}
}
}

View File

@ -4,6 +4,7 @@ import android.app.Application
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -34,6 +35,59 @@ import java.nio.file.StandardCopyOption
import kotlin.io.path.deleteExisting
import kotlin.io.path.inputStream
sealed class ResetDialogState(
@StringRes val titleResId: Int,
@StringRes val descriptionResId: Int,
val onConfirm: () -> Unit,
val dialogOptionName: String? = null
) {
class Keystore(onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.regenerate_keystore,
descriptionResId = R.string.regenerate_keystore_dialog_description,
onConfirm = onConfirm
)
class PatchSelectionAll(onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_selection_reset_all,
descriptionResId = R.string.patch_selection_reset_all_dialog_description,
onConfirm = onConfirm
)
class PatchSelectionPackage(dialogOptionName:String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_selection_reset_package,
descriptionResId = R.string.patch_selection_reset_package_dialog_description,
onConfirm = onConfirm,
dialogOptionName = dialogOptionName
)
class PatchSelectionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_selection_reset_bundle,
descriptionResId = R.string.patch_selection_reset_bundle_dialog_description,
onConfirm = onConfirm,
dialogOptionName = dialogOptionName
)
class PatchOptionsAll(onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_options_reset_all,
descriptionResId = R.string.patch_options_reset_all_dialog_description,
onConfirm = onConfirm
)
class PatchOptionPackage(dialogOptionName:String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_options_reset_package,
descriptionResId = R.string.patch_options_reset_package_dialog_description,
onConfirm = onConfirm,
dialogOptionName = dialogOptionName
)
class PatchOptionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_options_reset_bundle,
descriptionResId = R.string.patch_options_reset_bundle_dialog_description,
onConfirm = onConfirm,
dialogOptionName = dialogOptionName
)
}
@OptIn(ExperimentalSerializationApi::class)
class ImportExportViewModel(
private val app: Application,
@ -51,15 +105,18 @@ class ImportExportViewModel(
private var keystoreImportPath by mutableStateOf<Path?>(null)
val showCredentialsDialog by derivedStateOf { keystoreImportPath != null }
var resetDialogState by mutableStateOf<ResetDialogState?>(null)
val packagesWithOptions = optionsRepository.getPackagesWithSavedOptions()
val packagesWithSelection = selectionRepository.getPackagesWithSavedSelection()
fun resetOptionsForPackage(packageName: String) = viewModelScope.launch {
optionsRepository.clearOptionsForPackage(packageName)
optionsRepository.resetOptionsForPackage(packageName)
app.toast(app.getString(R.string.patch_options_reset_toast))
}
fun clearOptionsForBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
optionsRepository.clearOptionsForPatchBundle(patchBundle.uid)
fun resetOptionsForBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
optionsRepository.resetOptionsForPatchBundle(patchBundle.uid)
app.toast(app.getString(R.string.patch_options_reset_toast))
}
@ -135,6 +192,16 @@ class ImportExportViewModel(
app.toast(app.getString(R.string.reset_patch_selection_success))
}
fun resetSelectionForPackage(packageName: String) = viewModelScope.launch {
selectionRepository.resetSelectionForPackage(packageName)
app.toast(app.getString(R.string.reset_patch_selection_success))
}
fun resetSelectionForPatchBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
selectionRepository.resetSelectionForPatchBundle(patchBundle.uid)
app.toast(app.getString(R.string.reset_patch_selection_success))
}
fun executeSelectionAction(target: Uri) = viewModelScope.launch {
val source = selectedBundle!!
val action = selectionAction!!

View File

@ -305,7 +305,7 @@ class SelectedAppInfoViewModel(
if (!persistConfiguration) return@launch
viewModelScope.launch(Dispatchers.Default) {
selection?.let { selectionRepository.updateSelection(packageName, it) }
?: selectionRepository.clearSelection(packageName)
?: selectionRepository.resetSelectionForPackage(packageName)
optionsRepository.saveOptions(packageName, filteredOptions)
}

View File

@ -118,6 +118,7 @@
<string name="export_keystore_success">Exported keystore</string>
<string name="regenerate_keystore">Regenerate keystore</string>
<string name="regenerate_keystore_description">Generate a new keystore</string>
<string name="regenerate_keystore_dialog_description">You are about to regenerate your keystore the manager will use during the patching process.\n\nYou will not be able to update the previously installed apps from this source.</string>
<string name="regenerate_keystore_success">The keystore has been successfully replaced</string>
<string name="import_patch_selection">Import patch selection</string>
<string name="import_patch_selection_description">Import patch selection from a JSON file</string>
@ -129,12 +130,26 @@
<string name="export_patch_selection_success">Exported patch selection</string>
<string name="reset_patch_selection">Reset patch selection</string>
<string name="reset_patch_selection_description">Reset the stored patch selection</string>
<string name="reset_patch_options">Reset patch options</string>
<string name="reset_patch_options_description">Reset the stored patch options</string>
<string name="reset_patch_selection_success">Patch selection has been reset</string>
<string name="patch_selection_reset_all">Reset all patch selection</string>
<string name="patch_selection_reset_all_dialog_description">You are about to reset all the patch selections. You will need to manually select each patch again.</string>
<string name="patch_selection_reset_all_description">Reset all the patch selections</string>
<string name="patch_selection_reset_package">Reset patch selection for app</string>
<string name="patch_selection_reset_package_dialog_description">You are about to reset the patch selection for the app \"%s\". You will have to manually select each patch again.</string>
<string name="patch_selection_reset_package_description">Resets patch selection for a single app</string>
<string name="patch_selection_reset_bundle">Resets patch selection for bundle</string>
<string name="patch_selection_reset_bundle_dialog_description">You are about to reset the patch selection for the bundle \"%s\". You will have to manually select each patch again.</string>
<string name="patch_selection_reset_bundle_description">Resets the patch selection for all patches in a bundle</string>
<string name="patch_options_reset_package">Reset patch options for app</string>
<string name="patch_options_reset_package_dialog_description">You are about to reset the patch options for the app \"%s\". You will have to reapply each option again.</string>
<string name="patch_options_reset_package_description">Resets patch options for a single app</string>
<string name="patch_options_reset_bundle">Resets patch options for bundle</string>
<string name="patch_options_reset_bundle_dialog_description">You are about to reset the patch options for the bundle \"%s\". You will have to reapply each option again.</string>
<string name="patch_options_reset_bundle_description">Resets patch options for all patches in a bundle</string>
<string name="patch_options_reset_all">Reset patch options</string>
<string name="patch_options_reset_all_dialog_description">You are about to reset patch options. You will have to reapply each option again.</string>
<string name="patch_options_reset_all_description">Resets all patch options</string>
<string name="downloader_plugins">Plugins</string>
<string name="downloader_plugin_state_trusted">Trusted</string>