Compare commits

...

7 Commits

Author SHA1 Message Date
Ax333l
02851c0487 fix: Update aapt2 to fix patching issues 2026-03-01 00:39:30 +01:00
Ax333l
6ce5b672fe fix: Exclude desktop AAPT and disable R8 optimizations 2026-02-28 14:54:13 +01:00
planshim
2c3d436b25 fix: unexclude other destabilizers 2026-02-28 07:13:07 +03:00
semantic-release-bot
af564eab1b chore: Release v1.26.0-dev.28 [skip ci]
# app [1.26.0-dev.28](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.27...v1.26.0-dev.28) (2026-02-28)

### Features

* Add patch selection filters ([#2956](https://github.com/ReVanced/revanced-manager/issues/2956)) ([0be493d](0be493da66))
2026-02-28 04:04:06 +00:00
AunAli K.
0be493da66 feat: Add patch selection filters (#2956)
Co-authored-by: planshim <100317079+planshim@users.noreply.github.com>
2026-02-28 06:55:03 +03:00
semantic-release-bot
8f23114bc3 chore: Release v1.26.0-dev.27 [skip ci]
# app [1.26.0-dev.27](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.26...v1.26.0-dev.27) (2026-02-28)

### Features

* Show patch bundle version used to patch an app ([#2966](https://github.com/ReVanced/revanced-manager/issues/2966)) ([11bd46c](11bd46ce26))
2026-02-28 02:09:32 +00:00
AunAli K.
11bd46ce26 feat: Show patch bundle version used to patch an app (#2966)
Co-authored-by: planshim <100317079+planshim@users.noreply.github.com>
2026-02-28 05:01:23 +03:00
21 changed files with 963 additions and 29 deletions

View File

@@ -1,3 +1,17 @@
# app [1.26.0-dev.28](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.27...v1.26.0-dev.28) (2026-02-28)
### Features
* Add patch selection filters ([#2956](https://github.com/ReVanced/revanced-manager/issues/2956)) ([0be493d](https://github.com/ReVanced/revanced-manager/commit/0be493da662ed2b46d4b8616942357eb6a111d17))
# app [1.26.0-dev.27](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.26...v1.26.0-dev.27) (2026-02-28)
### Features
* Show patch bundle version used to patch an app ([#2966](https://github.com/ReVanced/revanced-manager/issues/2966)) ([11bd46c](https://github.com/ReVanced/revanced-manager/commit/11bd46ce26d4fbc7f126bee5e254d4f6b5e0cc43))
# app [1.26.0-dev.26](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.25...v1.26.0-dev.26) (2026-02-27)

View File

@@ -165,7 +165,6 @@ android {
buildTypes {
debug {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "ReVanced Manager (Debug)")
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
@@ -175,13 +174,12 @@ android {
if (!project.hasProperty("noProguard")) {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
val keystoreFile = file("keystore.jks")
if (project.hasProperty("signAsDebug") || !keystoreFile.exists()) {
applicationIdSuffix = ".debug_signed"
resValue("string", "app_name", "ReVanced Manager (Debug signed)")
signingConfig = signingConfigs.getByName("debug")
@@ -255,8 +253,8 @@ android {
excludes += "/META-INF/**/*.properties"
excludes += "/META-INF/DEPENDENCIES"
// Desktop AAPT binaries
excludes += "/prebuilt/**"
// AAPT
excludes += "/prebuilt/**/*"
}
jniLibs {
// 32-bit x86 is dead
@@ -268,6 +266,7 @@ android {
}
}
androidComponents {
onVariants(selector().withBuildType("release")) {
it.packaging.resources.excludes.apply {

View File

@@ -1 +1 @@
version = 1.26.0-dev.26
version = 1.26.0-dev.28

View File

@@ -1,4 +1,5 @@
-dontobfuscate
-keepattributes *
-keep class app.revanced.manager.patcher.runtime.process.* { *; }
-keep class app.revanced.manager.downloader.** { *; }

View File

@@ -0,0 +1,466 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "d9350f0e779a1bde46227a29884a7865",
"entities": [
{
"tableName": "patch_bundles",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` TEXT, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, PRIMARY KEY(`uid`))",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "versionHash",
"columnName": "version",
"affinity": "TEXT"
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "autoUpdate",
"columnName": "auto_update",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uid"
]
}
},
{
"tableName": "patch_selections",
"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_patch_selections_patch_bundle_package_name",
"unique": true,
"columnNames": [
"patch_bundle",
"package_name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_selections_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": "selected_patches",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`selection` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`selection`, `patch_name`), FOREIGN KEY(`selection`) REFERENCES `patch_selections`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "selection",
"columnName": "selection",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchName",
"columnName": "patch_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"selection",
"patch_name"
]
},
"foreignKeys": [
{
"table": "patch_selections",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"selection"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "downloaded_app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, `last_used` INTEGER NOT NULL, PRIMARY KEY(`package_name`, `version`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "directory",
"columnName": "directory",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastUsed",
"columnName": "last_used",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name",
"version"
]
}
},
{
"tableName": "installed_app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`current_package_name` TEXT NOT NULL, `original_package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `install_type` TEXT NOT NULL, PRIMARY KEY(`current_package_name`))",
"fields": [
{
"fieldPath": "currentPackageName",
"columnName": "current_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "originalPackageName",
"columnName": "original_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "installType",
"columnName": "install_type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"current_package_name"
]
}
},
{
"tableName": "applied_patch",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bundle",
"columnName": "bundle",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchName",
"columnName": "patch_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name",
"bundle",
"patch_name"
]
},
"indices": [
{
"name": "index_applied_patch_bundle",
"unique": false,
"columnNames": [
"bundle"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_applied_patch_bundle` ON `${TABLE_NAME}` (`bundle`)"
}
],
"foreignKeys": [
{
"table": "installed_app",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"package_name"
],
"referencedColumns": [
"current_package_name"
]
},
{
"table": "patch_bundles",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"bundle"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "installed_patch_bundle",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle_uid` INTEGER NOT NULL, `bundle_name` TEXT NOT NULL, `bundle_version` TEXT, PRIMARY KEY(`package_name`, `bundle_uid`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bundleUid",
"columnName": "bundle_uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bundleName",
"columnName": "bundle_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bundleVersion",
"columnName": "bundle_version",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name",
"bundle_uid"
]
},
"foreignKeys": [
{
"table": "installed_app",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"package_name"
],
"referencedColumns": [
"current_package_name"
]
}
]
},
{
"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"
]
},
"foreignKeys": [
{
"table": "option_groups",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"group"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "trusted_downloaders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` BLOB NOT NULL, PRIMARY KEY(`package_name`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "signature",
"columnName": "signature",
"affinity": "BLOB",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name"
]
}
}
],
"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, 'd9350f0e779a1bde46227a29884a7865')"
]
}
}

View File

@@ -1,13 +1,15 @@
package app.revanced.manager.data.room
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import app.revanced.manager.data.room.apps.downloaded.DownloadedAppDao
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.data.room.apps.downloaded.DownloadedAppDao
import app.revanced.manager.data.room.apps.installed.AppliedPatch
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.data.room.apps.installed.InstalledAppDao
import app.revanced.manager.data.room.apps.installed.InstalledPatchBundle
import app.revanced.manager.data.room.selection.PatchSelection
import app.revanced.manager.data.room.selection.SelectedPatch
import app.revanced.manager.data.room.selection.SelectionDao
@@ -21,8 +23,9 @@ import app.revanced.manager.data.room.downloader.TrustedDownloaderDao
import kotlin.random.Random
@Database(
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloader::class],
version = 1
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, InstalledPatchBundle::class, OptionGroup::class, Option::class, TrustedDownloader::class],
version = 2,
autoMigrations = [AutoMigration(from = 1, to = 2)]
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {

View File

@@ -25,11 +25,20 @@ interface InstalledAppDao {
"patch_name"
) String>>
@Query("SELECT * FROM installed_patch_bundle WHERE package_name = :packageName")
suspend fun getInstalledPatchBundles(packageName: String): List<InstalledPatchBundle>
@Transaction
suspend fun upsertApp(installedApp: InstalledApp, appliedPatches: List<AppliedPatch>) {
suspend fun upsertApp(
installedApp: InstalledApp,
appliedPatches: List<AppliedPatch>,
patchBundles: List<InstalledPatchBundle>
) {
upsertApp(installedApp)
deleteAppliedPatches(installedApp.currentPackageName)
deleteInstalledPatchBundles(installedApp.currentPackageName)
insertAppliedPatches(appliedPatches)
insertInstalledPatchBundles(patchBundles)
}
@Upsert
@@ -38,9 +47,15 @@ interface InstalledAppDao {
@Insert
suspend fun insertAppliedPatches(appliedPatches: List<AppliedPatch>)
@Insert
suspend fun insertInstalledPatchBundles(patchBundles: List<InstalledPatchBundle>)
@Query("DELETE FROM applied_patch WHERE package_name = :packageName")
suspend fun deleteAppliedPatches(packageName: String)
@Query("DELETE FROM installed_patch_bundle WHERE package_name = :packageName")
suspend fun deleteInstalledPatchBundles(packageName: String)
@Delete
suspend fun delete(installedApp: InstalledApp)
}

View File

@@ -0,0 +1,24 @@
package app.revanced.manager.data.room.apps.installed
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(
tableName = "installed_patch_bundle",
primaryKeys = ["package_name", "bundle_uid"],
foreignKeys = [
ForeignKey(
InstalledApp::class,
parentColumns = ["current_package_name"],
childColumns = ["package_name"],
onDelete = ForeignKey.CASCADE
)
]
)
data class InstalledPatchBundle(
@ColumnInfo(name = "package_name") val packageName: String,
@ColumnInfo(name = "bundle_uid") val bundleUid: Int,
@ColumnInfo(name = "bundle_name") val bundleName: String,
@ColumnInfo(name = "bundle_version") val bundleVersion: String?
)

View File

@@ -4,6 +4,8 @@ import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.apps.installed.AppliedPatch
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.data.room.apps.installed.InstalledPatchBundle
import app.revanced.manager.patcher.patch.PatchBundleInfo
import app.revanced.manager.util.PatchSelection
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -19,12 +21,16 @@ class InstalledAppRepository(
suspend fun getAppliedPatches(packageName: String): PatchSelection =
dao.getPatchesSelection(packageName).mapValues { (_, patches) -> patches.toSet() }
suspend fun getInstalledPatchBundles(packageName: String) =
dao.getInstalledPatchBundles(packageName)
suspend fun addOrUpdate(
currentPackageName: String,
originalPackageName: String,
version: String,
installType: InstallType,
patchSelection: PatchSelection
patchSelection: PatchSelection,
bundleInfo: Map<Int, PatchBundleInfo.Global> = emptyMap()
) {
dao.upsertApp(
InstalledApp(
@@ -41,6 +47,15 @@ class InstalledAppRepository(
patchName = patch
)
}
},
patchSelection.keys.mapNotNull { uid ->
val info = bundleInfo[uid] ?: return@mapNotNull null
InstalledPatchBundle(
packageName = currentPackageName,
bundleUid = uid,
bundleName = info.name,
bundleVersion = info.version
)
}
)
}

View File

@@ -0,0 +1,31 @@
package app.revanced.manager.ui.component.haptics
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.CheckboxColors
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.TriStateCheckbox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.state.ToggleableState
import app.revanced.manager.util.withHapticFeedback
@Composable
fun HapticTriStateCheckbox(
state: ToggleableState,
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
colors: CheckboxColors = CheckboxDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
TriStateCheckbox(
state = state,
onClick = onClick?.withHapticFeedback(HapticFeedbackConstants.CLOCK_TICK),
modifier = modifier,
enabled = enabled,
colors = colors,
interactionSource = interactionSource
)
}

View File

@@ -0,0 +1,74 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstalledPatchBundle
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.FullscreenDialog
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.transparentListItemColors
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppliedPatchesDialog(
onDismissRequest: () -> Unit,
appliedPatches: PatchSelection?,
patchBundles: List<InstalledPatchBundle>
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
FullscreenDialog(onDismissRequest = onDismissRequest) {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.applied_patches),
scrollBehavior = scrollBehavior,
onBackClick = onDismissRequest
)
},
) { paddingValues ->
LazyColumnWithScrollbar(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
val patches = appliedPatches ?: return@LazyColumnWithScrollbar
val bundleMap = patchBundles.associateBy { it.bundleUid }
patches.forEach { (bundleUid, patchNames) ->
val bundle = bundleMap[bundleUid]
item(key = "header_$bundleUid") {
ListHeader(title = bundle?.let {
it.bundleVersion?.let { version ->
"${it.bundleName} v$version"
} ?: it.bundleName
} ?: "${stringResource(R.string.patches)} $bundleUid")
}
items(
items = patchNames.sorted(),
key = { "${bundleUid}_$it" }
) { patchName ->
ListItem(
headlineContent = { Text(patchName) },
colors = transparentListItemColors
)
}
}
}
}
}
}

View File

@@ -32,7 +32,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -44,7 +43,6 @@ import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.SegmentedButton
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.InstalledAppInfoViewModel
import app.revanced.manager.util.toast
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -53,19 +51,26 @@ fun InstalledAppInfoScreen(
onBackClick: () -> Unit,
viewModel: InstalledAppInfoViewModel
) {
val context = LocalContext.current
SideEffect {
viewModel.onBackClick = onBackClick
}
var showUninstallDialog by rememberSaveable { mutableStateOf(false) }
var showAppliedPatchesDialog by rememberSaveable { mutableStateOf(false) }
if (showUninstallDialog)
UninstallDialog(
onDismiss = { showUninstallDialog = false },
onConfirm = { viewModel.uninstall() }
)
if (showAppliedPatchesDialog) {
AppliedPatchesDialog(
onDismissRequest = { showAppliedPatchesDialog = false },
appliedPatches = viewModel.appliedPatches,
patchBundles = viewModel.patchBundles
)
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@@ -152,7 +157,7 @@ fun InstalledAppInfoScreen(
modifier = Modifier.padding(vertical = 16.dp)
) {
SettingsListItem(
modifier = Modifier.clickable { context.toast("Not implemented yet!") },
modifier = Modifier.clickable(onClick = { showAppliedPatchesDialog = true }),
headlineContent = stringResource(R.string.applied_patches),
supportingContent =
(viewModel.appliedPatches?.values?.sumOf { it.size } ?: 0).let {

View File

@@ -15,9 +15,12 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
@@ -27,13 +30,16 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Deselect
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SwapHoriz
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
@@ -60,7 +66,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.dp
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.patcher.patch.Option
@@ -74,6 +82,7 @@ import app.revanced.manager.ui.component.SearchBar
import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticTab
import app.revanced.manager.ui.component.haptics.HapticTriStateCheckbox
import app.revanced.manager.ui.component.patches.OptionItem
import app.revanced.manager.ui.component.patches.SelectionWarningDialog
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
@@ -124,7 +133,39 @@ fun PatchesSelectorScreen(
val patchLazyListStates = remember(bundles) { List(bundles.size) { LazyListState() } }
var showSelectionWarning by rememberSaveable { mutableStateOf(false) }
var showUniversalWarning by rememberSaveable { mutableStateOf(false) }
var pendingScopeAction by remember { mutableStateOf<((Int?) -> Unit)?>(null) }
fun executeScopedAction(action: (Int?) -> Unit) {
if (bundles.size > 1) {
pendingScopeAction = action
} else {
action(bundles.firstOrNull()?.uid)
}
}
pendingScopeAction?.let { action ->
val currentBundle = bundles.getOrNull(pagerState.currentPage) ?: return@let
ScopeDialog(
bundleName = currentBundle.name,
onDismissRequest = { pendingScopeAction = null },
onAllPatches = {
action(null)
pendingScopeAction = null
},
onBundleOnly = {
action(currentBundle.uid)
pendingScopeAction = null
}
)
}
if (showBottomSheet) {
val currentBundle = bundles.getOrNull(pagerState.currentPage)
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
@@ -161,6 +202,71 @@ fun PatchesSelectorScreen(
label = { Text(stringResource(R.string.universal)) },
)
}
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
Text(
text = stringResource(R.string.patch_selector_sheet_actions_title),
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(bottom = 8.dp)
)
fun guardedAction(action: () -> Unit) {
showBottomSheet = false
if (viewModel.selectionWarningEnabled) {
showSelectionWarning = true
} else {
action()
}
}
ActionItem(
icon = Icons.Outlined.Restore,
text = stringResource(R.string.restore_default_selection),
onClick = {
guardedAction {
executeScopedAction { uid ->
viewModel.restoreDefaults(uid)
}
}
}
)
ActionItem(
icon = Icons.Outlined.Deselect,
text = stringResource(R.string.deselect_all),
onClick = {
guardedAction {
executeScopedAction { uid ->
viewModel.deselectAll(bundles, uid)
}
}
}
)
ActionItem(
icon = Icons.Outlined.SwapHoriz,
text = stringResource(R.string.invert_selection),
onClick = {
guardedAction {
executeScopedAction { uid ->
viewModel.invertSelection(bundles, uid)
}
}
}
)
if (bundles.size > 1 && currentBundle != null) {
ActionItem(
icon = Icons.Outlined.Deselect,
text = stringResource(R.string.deselect_all_except, currentBundle.name),
onClick = {
guardedAction {
viewModel.deselectAllExcept(bundles, currentBundle.uid)
}
}
)
}
}
}
}
@@ -191,9 +297,6 @@ fun PatchesSelectorScreen(
)
}
var showSelectionWarning by rememberSaveable { mutableStateOf(false) }
var showUniversalWarning by rememberSaveable { mutableStateOf(false) }
if (showSelectionWarning)
SelectionWarningDialog(onDismiss = { showSelectionWarning = false })
@@ -426,16 +529,38 @@ fun PatchesSelectorScreen(
}
},
text = {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = bundle.name,
style = MaterialTheme.typography.bodyMedium
)
Text(
text = bundle.version.orEmpty(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
Row(verticalAlignment = Alignment.CenterVertically) {
val selectionState = viewModel.getBundleSelectionState(bundle)
val toggleableState = when (selectionState) {
true -> ToggleableState.On
false -> ToggleableState.Off
null -> ToggleableState.Indeterminate
}
HapticTriStateCheckbox(
state = toggleableState,
onClick = {
when {
viewModel.selectionWarningEnabled -> showSelectionWarning = true
selectionState == false -> viewModel.restoreDefaults(bundle.uid)
else -> viewModel.deselectAll(bundles, bundle.uid)
}
}
)
Spacer(modifier = Modifier.width(4.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = bundle.name,
style = MaterialTheme.typography.bodyMedium
)
Text(
text = bundle.version.orEmpty(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
},
selectedContentColor = MaterialTheme.colorScheme.primary,
@@ -612,6 +737,41 @@ private fun IncompatiblePatchDialog(
}
)
@Composable
private fun ActionItem(
icon: ImageVector,
text: String,
onClick: () -> Unit
) {
ListItem(
modifier = Modifier.clickable(onClick = onClick),
leadingContent = { Icon(icon, contentDescription = null) },
headlineContent = { Text(text) },
colors = transparentListItemColors
)
}
@Composable
private fun ScopeDialog(
bundleName: String,
onDismissRequest: () -> Unit,
onAllPatches: () -> Unit,
onBundleOnly: () -> Unit
) = AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(stringResource(R.string.scope_dialog_title)) },
confirmButton = {
TextButton(onClick = onAllPatches) {
Text(stringResource(R.string.scope_all_patches))
}
},
dismissButton = {
TextButton(onClick = onBundleOnly) {
Text(stringResource(R.string.scope_bundle_patches, bundleName))
}
}
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun OptionsDialog(

View File

@@ -0,0 +1,37 @@
package app.revanced.manager.ui.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.room.apps.installed.InstalledPatchBundle
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.util.PatchSelection
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class AppliedPatchesViewModel(
packageName: String
) : ViewModel(), KoinComponent {
private val installedAppRepository: InstalledAppRepository by inject()
var appliedPatches: PatchSelection? by mutableStateOf(null)
private set
var patchBundles: List<InstalledPatchBundle> by mutableStateOf(emptyList())
private set
init {
viewModelScope.launch {
appliedPatches = withContext(Dispatchers.IO) {
installedAppRepository.getAppliedPatches(packageName)
}
patchBundles = withContext(Dispatchers.IO) {
installedAppRepository.getInstalledPatchBundles(packageName)
}
}
}
}

View File

@@ -11,6 +11,7 @@ import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.data.room.apps.installed.InstalledPatchBundle
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.util.PM
@@ -41,6 +42,8 @@ class InstalledAppInfoViewModel(
var appInfo: PackageInfo? by mutableStateOf(null)
private set
var appliedPatches: PatchSelection? by mutableStateOf(null)
var patchBundles: List<InstalledPatchBundle> by mutableStateOf(emptyList())
private set
var isMounted by mutableStateOf(false)
private set
@@ -54,6 +57,9 @@ class InstalledAppInfoViewModel(
appliedPatches = withContext(Dispatchers.IO) {
installedAppRepository.getAppliedPatches(it.currentPackageName)
}
patchBundles = withContext(Dispatchers.IO) {
installedAppRepository.getInstalledPatchBundles(it.currentPackageName)
}
}
}
}

View File

@@ -28,6 +28,7 @@ import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.patcher.ProgressEvent
import app.revanced.manager.patcher.StepId
@@ -59,6 +60,7 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.time.withTimeout
@@ -88,6 +90,7 @@ class PatcherViewModel(
private val pm: PM by inject()
private val workerRepository: WorkerRepository by inject()
private val installedAppRepository: InstalledAppRepository by inject()
private val patchBundleRepository: PatchBundleRepository by inject()
private val rootInstaller: RootInstaller by inject()
private val savedStateHandle: SavedStateHandle = get()
private val ackpineInstaller: PackageInstaller = get()
@@ -378,13 +381,15 @@ class PatcherViewModel(
Session.State.Succeeded -> {
app.toast(app.getString(R.string.install_app_success))
installedPackageName = installerPkgName
val bundleInfo = patchBundleRepository.bundleInfoFlow.first()
installedAppRepository.addOrUpdate(
installerPkgName,
packageName,
input.selectedApp.version
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
InstallType.DEFAULT,
input.selectedPatches
input.selectedPatches,
bundleInfo
)
}
}
@@ -457,12 +462,14 @@ class PatcherViewModel(
label
)
val bundleInfo = patchBundleRepository.bundleInfoFlow.first()
installedAppRepository.addOrUpdate(
currentPackageInfo.packageName,
packageName,
inputVersion,
InstallType.MOUNT,
input.selectedPatches
input.selectedPatches,
bundleInfo
)
rootInstaller.mount(packageName)

View File

@@ -209,6 +209,74 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
filter = filter xor flag
}
fun getBundleSelectionState(bundle: PatchBundleInfo.Scoped): Boolean? {
val patches = bundle.patchSequence(allowIncompatiblePatches).toList()
if (patches.isEmpty()) return false
val selectedCount = patches.count { isSelected(bundle.uid, it) }
return when (selectedCount) {
patches.size -> true
0 -> false
else -> null
}
}
private suspend fun currentSelection(): PersistentPatchSelection =
customPatchSelection ?: defaultPatchSelection.first()
private suspend fun updateSelection(
update: (PersistentPatchSelection) -> PersistentPatchSelection
) {
hasModifiedSelection = true
customPatchSelection = update(currentSelection())
}
fun deselectAll(bundles: List<PatchBundleInfo.Scoped>, bundleUid: Int?) = viewModelScope.launch {
updateSelection { selection ->
bundles.fold(selection) { acc, bundle ->
if (bundleUid != null && bundle.uid != bundleUid) return@fold acc
acc.put(bundle.uid, persistentSetOf())
}
}
}
fun invertSelection(bundles: List<PatchBundleInfo.Scoped>, bundleUid: Int?) = viewModelScope.launch {
updateSelection { selection ->
bundles.fold(selection) { acc, bundle ->
if (bundleUid != null && bundle.uid != bundleUid) return@fold acc
val currentSelected = acc[bundle.uid] ?: persistentSetOf()
val inverted = bundle.patchSequence(allowIncompatiblePatches)
.filter { it.name !in currentSelected }
.map { it.name }
.toPersistentSet()
acc.put(bundle.uid, inverted)
}
}
}
fun restoreDefaults(bundleUid: Int?) = viewModelScope.launch {
if (bundleUid == null) {
customPatchSelection = null
hasModifiedSelection = false
return@launch
}
val defaults = defaultPatchSelection.first()
updateSelection { selection ->
selection.put(bundleUid, defaults[bundleUid] ?: persistentSetOf())
}
}
fun deselectAllExcept(bundles: List<PatchBundleInfo.Scoped>, keepBundleUid: Int) = viewModelScope.launch {
updateSelection { selection ->
bundles.fold(selection) { acc, bundle ->
if (bundle.uid == keepBundleUid) return@fold acc
acc.put(bundle.uid, persistentSetOf())
}
}
}
companion object {
const val SHOW_INCOMPATIBLE = 1 // 2^0
const val SHOW_UNIVERSAL = 2 // 2^1

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -329,6 +329,15 @@ It is only compatible with the following version(s): %2$s</string>
<string name="patch_selector_sheet_filter_title">Filter</string>
<string name="patch_selector_sheet_filter_compat_title">Compatibility</string>
<string name="patch_selector_sheet_actions_title">Actions</string>
<string name="restore_default_selection">Restore default selection</string>
<string name="deselect_all">Deselect all</string>
<string name="invert_selection">Invert selection</string>
<string name="deselect_all_except">Deselect all except %s</string>
<string name="scope_dialog_title">Apply to</string>
<string name="scope_all_patches">All patches</string>
<string name="scope_bundle_patches">%s only</string>
<string name="string_option_menu_description">More options</string>
<string name="option_preset_custom_value">Custom value</string>