mirror of
https://github.com/revanced/revanced-manager
synced 2024-05-14 13:56:57 +02:00
feat: remember patch options (#1449)
This commit is contained in:
parent
123ae37524
commit
7fe4724e10
@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "371c7a84b122a2de8b660b35e6e9ce14",
|
||||
"identityHash": "802fa2fda94b930bf0ebb85d195f1022",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "patch_bundles",
|
||||
@ -295,12 +295,119 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "option_groups",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uid",
|
||||
"columnName": "uid",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "patchBundle",
|
||||
"columnName": "patch_bundle",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "package_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"uid"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_option_groups_patch_bundle_package_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"patch_bundle",
|
||||
"package_name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_option_groups_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "patch_bundles",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"patch_bundle"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uid"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "options",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`group`, `patch_name`, `key`), FOREIGN KEY(`group`) REFERENCES `option_groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "group",
|
||||
"columnName": "group",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "patchName",
|
||||
"columnName": "patch_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"group",
|
||||
"patch_name",
|
||||
"key"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "option_groups",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"group"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uid"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '371c7a84b122a2de8b660b35e6e9ce14')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '802fa2fda94b930bf0ebb85d195f1022')"
|
||||
]
|
||||
}
|
||||
}
|
@ -13,15 +13,19 @@ import app.revanced.manager.data.room.selection.SelectedPatch
|
||||
import app.revanced.manager.data.room.selection.SelectionDao
|
||||
import app.revanced.manager.data.room.bundles.PatchBundleDao
|
||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||
import app.revanced.manager.data.room.options.Option
|
||||
import app.revanced.manager.data.room.options.OptionDao
|
||||
import app.revanced.manager.data.room.options.OptionGroup
|
||||
import kotlin.random.Random
|
||||
|
||||
@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class], version = 1)
|
||||
@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class], version = 1)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun patchBundleDao(): PatchBundleDao
|
||||
abstract fun selectionDao(): SelectionDao
|
||||
abstract fun downloadedAppDao(): DownloadedAppDao
|
||||
abstract fun installedAppDao(): InstalledAppDao
|
||||
abstract fun optionDao(): OptionDao
|
||||
|
||||
companion object {
|
||||
fun generateUid() = Random.Default.nextInt()
|
||||
|
@ -0,0 +1,23 @@
|
||||
package app.revanced.manager.data.room.options
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
|
||||
@Entity(
|
||||
tableName = "options",
|
||||
primaryKeys = ["group", "patch_name", "key"],
|
||||
foreignKeys = [ForeignKey(
|
||||
OptionGroup::class,
|
||||
parentColumns = ["uid"],
|
||||
childColumns = ["group"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)]
|
||||
)
|
||||
data class Option(
|
||||
@ColumnInfo(name = "group") val group: Int,
|
||||
@ColumnInfo(name = "patch_name") val patchName: String,
|
||||
@ColumnInfo(name = "key") val key: String,
|
||||
// Encoded as Json.
|
||||
@ColumnInfo(name = "value") val value: String,
|
||||
)
|
@ -0,0 +1,51 @@
|
||||
package app.revanced.manager.data.room.options
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.MapInfo
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
abstract class OptionDao {
|
||||
@Transaction
|
||||
@MapInfo(keyColumn = "patch_bundle")
|
||||
@Query(
|
||||
"SELECT patch_bundle, `group`, patch_name, `key`, value FROM option_groups" +
|
||||
" LEFT JOIN options ON uid = options.`group`" +
|
||||
" WHERE package_name = :packageName"
|
||||
)
|
||||
abstract suspend fun getOptions(packageName: String): Map<Int, List<Option>>
|
||||
|
||||
@Query("SELECT uid FROM option_groups WHERE patch_bundle = :bundleUid AND package_name = :packageName")
|
||||
abstract suspend fun getGroupId(bundleUid: Int, packageName: String): Int?
|
||||
|
||||
@Query("SELECT package_name FROM option_groups")
|
||||
abstract fun getPackagesWithOptions(): Flow<List<String>>
|
||||
|
||||
@Insert
|
||||
abstract suspend fun createOptionGroup(group: OptionGroup)
|
||||
|
||||
@Query("DELETE FROM option_groups WHERE patch_bundle = :uid")
|
||||
abstract suspend fun clearForPatchBundle(uid: Int)
|
||||
|
||||
@Query("DELETE FROM option_groups WHERE package_name = :packageName")
|
||||
abstract suspend fun clearForPackage(packageName: String)
|
||||
|
||||
@Query("DELETE FROM option_groups")
|
||||
abstract suspend fun reset()
|
||||
|
||||
@Insert
|
||||
protected abstract suspend fun insertOptions(patches: List<Option>)
|
||||
|
||||
@Query("DELETE FROM options WHERE `group` = :groupId")
|
||||
protected abstract suspend fun clearGroup(groupId: Int)
|
||||
|
||||
@Transaction
|
||||
open suspend fun updateOptions(options: Map<Int, List<Option>>) =
|
||||
options.forEach { (groupId, options) ->
|
||||
clearGroup(groupId)
|
||||
insertOptions(options)
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package app.revanced.manager.data.room.options
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||
|
||||
@Entity(
|
||||
tableName = "option_groups",
|
||||
foreignKeys = [ForeignKey(
|
||||
PatchBundleEntity::class,
|
||||
parentColumns = ["uid"],
|
||||
childColumns = ["patch_bundle"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
indices = [Index(value = ["patch_bundle", "package_name"], unique = true)]
|
||||
)
|
||||
data class OptionGroup(
|
||||
@PrimaryKey val uid: Int,
|
||||
@ColumnInfo(name = "patch_bundle") val patchBundle: Int,
|
||||
@ColumnInfo(name = "package_name") val packageName: String
|
||||
)
|
@ -49,7 +49,7 @@ abstract class SelectionDao {
|
||||
|
||||
@Transaction
|
||||
open suspend fun updateSelections(selections: Map<Int, Set<String>>) =
|
||||
selections.map { (selectionUid, patches) ->
|
||||
selections.forEach { (selectionUid, patches) ->
|
||||
clearSelection(selectionUid)
|
||||
selectPatches(patches.map { SelectedPatch(selectionUid, it) })
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ val repositoryModule = module {
|
||||
singleOf(::NetworkInfo)
|
||||
singleOf(::PatchBundlePersistenceRepository)
|
||||
singleOf(::PatchSelectionRepository)
|
||||
singleOf(::PatchOptionsRepository)
|
||||
singleOf(::PatchBundleRepository)
|
||||
singleOf(::WorkerRepository)
|
||||
singleOf(::DownloadedAppRepository)
|
||||
|
@ -0,0 +1,89 @@
|
||||
package app.revanced.manager.domain.repository
|
||||
|
||||
import app.revanced.manager.data.room.AppDatabase
|
||||
import app.revanced.manager.data.room.options.Option
|
||||
import app.revanced.manager.data.room.options.OptionGroup
|
||||
import app.revanced.manager.util.Options
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.floatOrNull
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
|
||||
class PatchOptionsRepository(db: AppDatabase) {
|
||||
private val dao = db.optionDao()
|
||||
|
||||
private suspend fun getOrCreateGroup(bundleUid: Int, packageName: String) =
|
||||
dao.getGroupId(bundleUid, packageName) ?: OptionGroup(
|
||||
uid = AppDatabase.generateUid(),
|
||||
patchBundle = bundleUid,
|
||||
packageName = packageName
|
||||
).also { dao.createOptionGroup(it) }.uid
|
||||
|
||||
suspend fun getOptions(packageName: String): Options {
|
||||
val options = dao.getOptions(packageName)
|
||||
// Bundle -> Patches
|
||||
return buildMap<Int, MutableMap<String, MutableMap<String, Any?>>>(options.size) {
|
||||
options.forEach { (sourceUid, bundlePatchOptionsList) ->
|
||||
// Patches -> Patch options
|
||||
this[sourceUid] = bundlePatchOptionsList.fold(mutableMapOf()) { bundlePatchOptions, option ->
|
||||
val patchOptions = bundlePatchOptions.getOrPut(option.patchName, ::mutableMapOf)
|
||||
|
||||
patchOptions[option.key] = deserialize(option.value)
|
||||
|
||||
bundlePatchOptions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveOptions(packageName: String, options: Options) =
|
||||
dao.updateOptions(options.entries.associate { (sourceUid, bundlePatchOptions) ->
|
||||
val groupId = getOrCreateGroup(sourceUid, packageName)
|
||||
|
||||
groupId to bundlePatchOptions.flatMap { (patchName, patchOptions) ->
|
||||
patchOptions.mapNotNull { (key, value) ->
|
||||
val serialized = serialize(value)
|
||||
?: return@mapNotNull null // Don't save options that we can't serialize.
|
||||
|
||||
Option(groupId, patchName, key, serialized)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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 reset() = dao.reset()
|
||||
|
||||
private companion object {
|
||||
fun deserialize(value: String): Any? {
|
||||
val primitive = Json.decodeFromString<JsonPrimitive>(value)
|
||||
|
||||
return when {
|
||||
primitive.isString -> primitive.content
|
||||
primitive is JsonNull -> null
|
||||
else -> primitive.booleanOrNull ?: primitive.intOrNull ?: primitive.floatOrNull
|
||||
}
|
||||
}
|
||||
|
||||
fun serialize(value: Any?): String? {
|
||||
val primitive = when (value) {
|
||||
null -> JsonNull
|
||||
is String -> JsonPrimitive(value)
|
||||
is Int -> JsonPrimitive(value)
|
||||
is Float -> JsonPrimitive(value)
|
||||
is Boolean -> JsonPrimitive(value)
|
||||
else -> return null
|
||||
}
|
||||
|
||||
return Json.encodeToString(primitive)
|
||||
}
|
||||
}
|
||||
}
|
@ -179,11 +179,11 @@ class PatcherWorker(
|
||||
.mapValues { (_, bundle) -> bundle.patchClasses(args.packageName) }
|
||||
|
||||
// Set all patch options.
|
||||
args.options.forEach { (bundle, configuredPatchOptions) ->
|
||||
args.options.forEach { (bundle, bundlePatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
configuredPatchOptions.forEach { (patchName, options) ->
|
||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||
val patchOptions = patches.single { it.name == patchName }.options
|
||||
options.forEach { (key, value) ->
|
||||
configuredPatchOptions.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
}
|
||||
|
@ -301,10 +301,7 @@ fun PatchesSelectorScreen(
|
||||
icon = { Icon(Icons.Outlined.Save, null) },
|
||||
onClick = {
|
||||
// TODO: only allow this if all required options have been set.
|
||||
composableScope.launch {
|
||||
vm.saveSelection()
|
||||
onSave(vm.getCustomSelection(), vm.getOptions())
|
||||
}
|
||||
onSave(vm.getCustomSelection(), vm.getOptions())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -49,9 +49,13 @@ fun SelectedAppInfoScreen(
|
||||
vm: SelectedAppInfoViewModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val bundles by remember(vm.selectedApp.packageName, vm.selectedApp.version) {
|
||||
vm.bundlesRepo.bundleInfoFlow(vm.selectedApp.packageName, vm.selectedApp.version)
|
||||
|
||||
val packageName = vm.selectedApp.packageName
|
||||
val version = vm.selectedApp.version
|
||||
val bundles by remember(packageName, version) {
|
||||
vm.bundlesRepo.bundleInfoFlow(packageName, version)
|
||||
}.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
val allowExperimental by vm.prefs.allowExperimental.getAsState()
|
||||
val patches by remember {
|
||||
derivedStateOf {
|
||||
@ -86,7 +90,7 @@ fun SelectedAppInfoScreen(
|
||||
onPatchClick(
|
||||
vm.selectedApp,
|
||||
patches,
|
||||
vm.patchOptions
|
||||
vm.getOptionsFiltered(bundles)
|
||||
)
|
||||
},
|
||||
onPatchSelectorClick = {
|
||||
@ -97,7 +101,7 @@ fun SelectedAppInfoScreen(
|
||||
bundles,
|
||||
allowExperimental
|
||||
),
|
||||
vm.patchOptions
|
||||
vm.options
|
||||
)
|
||||
)
|
||||
},
|
||||
@ -107,8 +111,8 @@ fun SelectedAppInfoScreen(
|
||||
onBackClick = onBackClick,
|
||||
availablePatchCount = availablePatchCount,
|
||||
selectedPatchCount = selectedPatchCount,
|
||||
packageName = vm.selectedApp.packageName,
|
||||
version = vm.selectedApp.version,
|
||||
packageName = packageName,
|
||||
version = version,
|
||||
packageInfo = vm.selectedAppInfo,
|
||||
)
|
||||
|
||||
@ -118,13 +122,12 @@ fun SelectedAppInfoScreen(
|
||||
vm.selectedApp = it
|
||||
navController.pop()
|
||||
},
|
||||
viewModel = getViewModel { parametersOf(vm.selectedApp.packageName) }
|
||||
viewModel = getViewModel { parametersOf(packageName) }
|
||||
)
|
||||
|
||||
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
|
||||
onSave = { patches, options ->
|
||||
vm.setCustomPatches(patches)
|
||||
vm.patchOptions = options
|
||||
vm.updateConfiguration(patches, options, bundles)
|
||||
navController.pop()
|
||||
},
|
||||
onBackClick = navController::pop,
|
||||
@ -133,7 +136,7 @@ fun SelectedAppInfoScreen(
|
||||
PatchesSelectorViewModel.Params(
|
||||
destination.app,
|
||||
destination.currentSelection,
|
||||
destination.options
|
||||
destination.options,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -6,7 +6,10 @@ import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@ -14,6 +17,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Key
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
@ -53,8 +57,10 @@ fun ImportExportSettingsScreen(
|
||||
it?.let(vm::exportKeystore)
|
||||
}
|
||||
|
||||
val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
|
||||
|
||||
vm.selectionAction?.let { action ->
|
||||
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val launcher = rememberLauncherForActivityResult(action.activityContract) { uri ->
|
||||
if (uri == null) {
|
||||
vm.clearSelectionAction()
|
||||
@ -64,7 +70,7 @@ fun ImportExportSettingsScreen(
|
||||
}
|
||||
|
||||
if (vm.selectedBundle == null) {
|
||||
BundleSelector(sources) {
|
||||
BundleSelector(patchBundles) {
|
||||
if (it == null) {
|
||||
vm.clearSelectionAction()
|
||||
} else {
|
||||
@ -137,11 +143,110 @@ fun ImportExportSettingsScreen(
|
||||
headline = R.string.backup_patches_selection,
|
||||
description = R.string.backup_patches_selection_description
|
||||
)
|
||||
// TODO: allow resetting selection for specific bundle or package name.
|
||||
GroupItem(
|
||||
onClick = vm::resetSelection,
|
||||
headline = R.string.clear_patches_selection,
|
||||
description = R.string.clear_patches_selection_description
|
||||
)
|
||||
|
||||
var showPackageSelector by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var showBundleSelector by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (showPackageSelector)
|
||||
PackageSelector(packages = packagesWithOptions) { selected ->
|
||||
selected?.let(vm::clearOptionsForPackage)
|
||||
|
||||
showPackageSelector = false
|
||||
}
|
||||
|
||||
if (showBundleSelector)
|
||||
BundleSelector(bundles = patchBundles) { bundle ->
|
||||
bundle?.let(vm::clearOptionsForBundle)
|
||||
|
||||
showBundleSelector = false
|
||||
}
|
||||
|
||||
GroupHeader(stringResource(R.string.patch_options))
|
||||
// TODO: patch options import/export.
|
||||
GroupItem(
|
||||
onClick = { showPackageSelector = true },
|
||||
headline = R.string.patch_options_clear_package,
|
||||
description = R.string.patch_options_clear_package_description
|
||||
)
|
||||
if (patchBundles.size > 1)
|
||||
GroupItem(
|
||||
onClick = { showBundleSelector = true },
|
||||
headline = R.string.patch_options_clear_bundle,
|
||||
description = R.string.patch_options_clear_bundle_description,
|
||||
)
|
||||
GroupItem(
|
||||
onClick = vm::resetOptions,
|
||||
headline = R.string.patch_options_clear_all,
|
||||
description = R.string.patch_options_clear_all_description,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun PackageSelector(packages: Set<String>, onFinish: (String?) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val noPackages = packages.isEmpty()
|
||||
|
||||
LaunchedEffect(noPackages) {
|
||||
if (noPackages) {
|
||||
context.toast("No packages available.")
|
||||
onFinish(null)
|
||||
}
|
||||
}
|
||||
|
||||
if (noPackages) return
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { onFinish(null) }
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Select package",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
packages.forEach {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onFinish(it)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||
import app.revanced.manager.domain.repository.SerializedSelection
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||
import app.revanced.manager.util.JSON_MIMETYPE
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
@ -38,10 +39,11 @@ class ImportExportViewModel(
|
||||
private val app: Application,
|
||||
private val keystoreManager: KeystoreManager,
|
||||
private val selectionRepository: PatchSelectionRepository,
|
||||
private val optionsRepository: PatchOptionsRepository,
|
||||
patchBundleRepository: PatchBundleRepository
|
||||
) : ViewModel() {
|
||||
private val contentResolver = app.contentResolver
|
||||
val sources = patchBundleRepository.sources
|
||||
val patchBundles = patchBundleRepository.sources
|
||||
var selectedBundle by mutableStateOf<PatchBundleSource?>(null)
|
||||
private set
|
||||
var selectionAction by mutableStateOf<SelectionAction?>(null)
|
||||
@ -49,6 +51,20 @@ class ImportExportViewModel(
|
||||
private var keystoreImportPath by mutableStateOf<Path?>(null)
|
||||
val showCredentialsDialog by derivedStateOf { keystoreImportPath != null }
|
||||
|
||||
val packagesWithOptions = optionsRepository.getPackagesWithSavedOptions()
|
||||
|
||||
fun clearOptionsForPackage(packageName: String) = viewModelScope.launch {
|
||||
optionsRepository.clearOptionsForPackage(packageName)
|
||||
}
|
||||
|
||||
fun clearOptionsForBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
|
||||
optionsRepository.clearOptionsForPatchBundle(patchBundle.uid)
|
||||
}
|
||||
|
||||
fun resetOptions() = viewModelScope.launch {
|
||||
optionsRepository.reset()
|
||||
}
|
||||
|
||||
fun startKeystoreImport(content: Uri) = viewModelScope.launch {
|
||||
val path = withContext(Dispatchers.IO) {
|
||||
File.createTempFile("signing", "ks", app.cacheDir).toPath().also {
|
||||
|
@ -17,7 +17,6 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||
import androidx.lifecycle.viewmodel.compose.saveable
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.ui.model.BundleInfo
|
||||
@ -31,20 +30,17 @@ import app.revanced.manager.util.saver.persistentMapSaver
|
||||
import app.revanced.manager.util.saver.persistentSetSaver
|
||||
import app.revanced.manager.util.saver.snapshotStateMapSaver
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import kotlinx.collections.immutable.*
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Optional
|
||||
|
||||
@Stable
|
||||
@OptIn(SavedStateHandleSaveableApi::class)
|
||||
class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||
private val app: Application = get()
|
||||
private val selectionRepository: PatchSelectionRepository = get()
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
private val prefs: PreferencesManager = get()
|
||||
|
||||
@ -169,11 +165,6 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||
return patchOptions.mapValues { (_, allPatches) -> allPatches.mapValues { (_, options) -> options.toMap() } }
|
||||
}
|
||||
|
||||
suspend fun saveSelection() = withContext(Dispatchers.Default) {
|
||||
customPatchesSelection?.let { selectionRepository.updateSelection(packageName, it) }
|
||||
?: selectionRepository.clearSelection(packageName)
|
||||
}
|
||||
|
||||
fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
|
||||
|
||||
fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) {
|
||||
|
@ -13,6 +13,7 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||
import androidx.lifecycle.viewmodel.compose.saveable
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||
import app.revanced.manager.ui.model.BundleInfo
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
||||
@ -31,10 +32,13 @@ import org.koin.core.component.get
|
||||
class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||
val bundlesRepo: PatchBundleRepository = get()
|
||||
private val selectionRepository: PatchSelectionRepository = get()
|
||||
private val optionsRepository: PatchOptionsRepository = get()
|
||||
private val pm: PM = get()
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
val prefs: PreferencesManager = get()
|
||||
|
||||
private val persistConfiguration = input.patches == null
|
||||
|
||||
private var _selectedApp by savedStateHandle.saveable {
|
||||
mutableStateOf(input.app)
|
||||
}
|
||||
@ -52,9 +56,18 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||
invalidateSelectedAppInfo()
|
||||
}
|
||||
|
||||
var patchOptions: Options by savedStateHandle.saveable {
|
||||
mutableStateOf(emptyMap())
|
||||
var options: Options by savedStateHandle.saveable {
|
||||
val state = mutableStateOf<Options>(emptyMap())
|
||||
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
||||
|
||||
state.value = optionsRepository.getOptions(selectedApp.packageName)
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
private set
|
||||
|
||||
private var selectionState by savedStateHandle.saveable {
|
||||
if (input.patches != null) {
|
||||
@ -87,6 +100,8 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||
selectedAppInfo = info
|
||||
}
|
||||
|
||||
fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles)
|
||||
|
||||
fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||
selectionState.patches(bundles, allowUnsupported)
|
||||
|
||||
@ -96,14 +111,56 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||
): PatchesSelection? =
|
||||
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
|
||||
|
||||
fun setCustomPatches(selection: PatchesSelection?) {
|
||||
fun updateConfiguration(
|
||||
selection: PatchesSelection?,
|
||||
options: Options,
|
||||
bundles: List<BundleInfo>
|
||||
) {
|
||||
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
||||
|
||||
val filteredOptions = options.filtered(bundles)
|
||||
this.options = filteredOptions
|
||||
|
||||
if (!persistConfiguration) return
|
||||
|
||||
val packageName = selectedApp.packageName
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
||||
?: selectionRepository.clearSelection(packageName)
|
||||
|
||||
optionsRepository.saveOptions(packageName, filteredOptions)
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val app: SelectedApp,
|
||||
val patches: PatchesSelection?,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
/**
|
||||
* Returns a copy with all nonexistent options removed.
|
||||
*/
|
||||
private fun Options.filtered(bundles: List<BundleInfo>): Options = buildMap options@{
|
||||
bundles.forEach bundles@{ bundle ->
|
||||
val bundleOptions = this@filtered[bundle.uid] ?: return@bundles
|
||||
|
||||
val patches = bundle.all.associateBy { it.name }
|
||||
|
||||
this@options[bundle.uid] = buildMap bundleOptions@{
|
||||
bundleOptions.forEach patch@{ (patchName, values) ->
|
||||
// Get all valid option keys for the patch.
|
||||
val validOptionKeys =
|
||||
patches[patchName]?.options?.map { it.key }?.toSet() ?: return@patch
|
||||
|
||||
this@bundleOptions[patchName] = values.filterKeys { key ->
|
||||
key in validOptionKeys
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed interface SelectionState : Parcelable {
|
||||
|
@ -90,6 +90,13 @@
|
||||
<string name="backup_patches_selection_fail">Failed to backup patches selection: %s</string>
|
||||
<string name="clear_patches_selection">Clear patches selection</string>
|
||||
<string name="clear_patches_selection_description">Clear all patches selection</string>
|
||||
<string name="patch_options">Patch options</string>
|
||||
<string name="patch_options_clear_package">Clear patch options for package</string>
|
||||
<string name="patch_options_clear_package_description">Resets patch options for a single package</string>
|
||||
<string name="patch_options_clear_bundle">Clear patch options for bundle</string>
|
||||
<string name="patch_options_clear_bundle_description">Resets patch options for all patches in a bundle</string>
|
||||
<string name="patch_options_clear_all">Clear all patch options</string>
|
||||
<string name="patch_options_clear_all_description">Resets all patch options</string>
|
||||
<string name="prefer_splits">Prefer split apks</string>
|
||||
<string name="prefer_splits_description">Prefer split apks instead of full apks</string>
|
||||
<string name="prefer_universal">Prefer universal apks</string>
|
||||
|
Loading…
Reference in New Issue
Block a user