mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-03-13 08:41:57 +08:00
Compare commits
8 Commits
v1.26.0-de
...
v1.26.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af564eab1b | ||
|
|
0be493da66 | ||
|
|
8f23114bc3 | ||
|
|
11bd46ce26 | ||
|
|
e03f3666c8 | ||
|
|
323d2170ef | ||
|
|
940669b3e4 | ||
|
|
81e5a9315d |
@@ -1,3 +1,31 @@
|
||||
# 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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* improve InstalledAppsScreen caching/perf ([#2963](https://github.com/ReVanced/revanced-manager/issues/2963)) ([323d217](https://github.com/ReVanced/revanced-manager/commit/323d2170efa0e3f2c8d2245ff08a2e92c52cb9d7))
|
||||
|
||||
# app [1.26.0-dev.25](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.24...v1.26.0-dev.25) (2026-02-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* improve AppSelectorScreen caching/perf ([#2961](https://github.com/ReVanced/revanced-manager/issues/2961)) ([81e5a93](https://github.com/ReVanced/revanced-manager/commit/81e5a9315db4c858fa29163d15457af97163967a))
|
||||
|
||||
# app [1.26.0-dev.24](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.23...v1.26.0-dev.24) (2026-02-27)
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 1.26.0-dev.24
|
||||
version = 1.26.0-dev.28
|
||||
|
||||
466
app/schemas/app.revanced.manager.data.room.AppDatabase/2.json
Normal file
466
app/schemas/app.revanced.manager.data.room.AppDatabase/2.json
Normal 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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -24,9 +24,9 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -68,20 +68,11 @@ fun AppSelectorScreen(
|
||||
uri?.let(vm::handleStorageResult)
|
||||
}
|
||||
|
||||
val suggestedVersions by vm.suggestedAppVersions.collectAsStateWithLifecycle(emptyMap())
|
||||
val suggestedVersions by vm.suggestedAppVersions.collectAsStateWithLifecycle()
|
||||
|
||||
var filterText by rememberSaveable { mutableStateOf("") }
|
||||
var search by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val appList by vm.appList.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val filteredAppList = remember(appList, filterText) {
|
||||
appList.filter { app ->
|
||||
(vm.loadLabel(app.packageInfo)).contains(
|
||||
filterText,
|
||||
true
|
||||
) or app.packageName.contains(filterText, true)
|
||||
}
|
||||
}
|
||||
val appList by vm.apps.collectAsStateWithLifecycle()
|
||||
val appsListFiltered by vm.filteredApps.collectAsStateWithLifecycle()
|
||||
|
||||
vm.nonSuggestedVersionDialogSubject?.let {
|
||||
NonSuggestedVersionDialog(
|
||||
@@ -90,17 +81,20 @@ fun AppSelectorScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (search)
|
||||
if (search) {
|
||||
val filterText by vm.filterText.collectAsState()
|
||||
|
||||
SearchView(
|
||||
query = filterText,
|
||||
onQueryChange = { filterText = it },
|
||||
onQueryChange = vm::setFilterText,
|
||||
onActiveChange = { search = it },
|
||||
placeholder = { Text(stringResource(R.string.search_apps)) }
|
||||
) {
|
||||
if (appList.isNotEmpty() && filterText.isNotEmpty()) {
|
||||
val appsFiltered = appsListFiltered
|
||||
if (!appsFiltered.isNullOrEmpty() && filterText.isNotEmpty()) {
|
||||
LazyColumnWithScrollbar(modifier = Modifier.fillMaxSize()) {
|
||||
items(
|
||||
items = filteredAppList,
|
||||
items = appsFiltered,
|
||||
key = { it.packageName }
|
||||
) { app ->
|
||||
ListItem(
|
||||
@@ -109,9 +103,9 @@ fun AppSelectorScreen(
|
||||
},
|
||||
leadingContent = {
|
||||
AppIcon(
|
||||
app.packageInfo,
|
||||
null,
|
||||
Modifier.size(36.dp)
|
||||
packageInfo = app.packageInfo,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(36.dp)
|
||||
)
|
||||
},
|
||||
headlineContent = { AppLabel(app.packageInfo) },
|
||||
@@ -152,6 +146,7 @@ fun AppSelectorScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
@@ -198,16 +193,33 @@ fun AppSelectorScreen(
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (appList.isNotEmpty()) {
|
||||
val apps = appList
|
||||
if (apps == null) {
|
||||
item(key = "LOADING") {
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
LoadingIndicator()
|
||||
}
|
||||
}
|
||||
} else if (apps.isNotEmpty()) {
|
||||
items(
|
||||
items = appList,
|
||||
key = { it.packageName }
|
||||
items = apps,
|
||||
key = { "APP-" + it.packageName },
|
||||
contentType = { "APP" },
|
||||
) { app ->
|
||||
ListItem(
|
||||
modifier = Modifier.clickable {
|
||||
onSelect(app.packageName)
|
||||
},
|
||||
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) },
|
||||
leadingContent = {
|
||||
AppIcon(
|
||||
packageInfo = app.packageInfo,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(36.dp)
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
AppLabel(
|
||||
app.packageInfo,
|
||||
@@ -231,15 +243,6 @@ fun AppSelectorScreen(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center
|
||||
) {
|
||||
LoadingIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -30,7 +31,7 @@ fun InstalledAppsScreen(
|
||||
onAppClick: (InstalledApp) -> Unit,
|
||||
viewModel: InstalledAppsViewModel = koinViewModel()
|
||||
) {
|
||||
val installedApps by viewModel.apps.collectAsStateWithLifecycle(initialValue = null)
|
||||
val installedApps by viewModel.apps.collectAsStateWithLifecycle()
|
||||
|
||||
Column {
|
||||
LazyColumnWithScrollbar(
|
||||
@@ -38,38 +39,40 @@ fun InstalledAppsScreen(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = if (installedApps.isNullOrEmpty()) Arrangement.Center else Arrangement.Top,
|
||||
) {
|
||||
installedApps?.let { installedApps ->
|
||||
if (installedApps.isNotEmpty()) {
|
||||
items(
|
||||
installedApps,
|
||||
key = { it.currentPackageName }
|
||||
) { installedApp ->
|
||||
viewModel.packageInfoMap[installedApp.currentPackageName].let { packageInfo ->
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { onAppClick(installedApp) },
|
||||
leadingContent = {
|
||||
AppIcon(
|
||||
packageInfo,
|
||||
contentDescription = null,
|
||||
Modifier.size(36.dp)
|
||||
)
|
||||
},
|
||||
headlineContent = { AppLabel(packageInfo, defaultText = null) },
|
||||
supportingContent = { Text(installedApp.currentPackageName) }
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.no_patched_apps_found),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
val apps = installedApps
|
||||
if (apps == null) {
|
||||
item(key = "LOADING") {
|
||||
LoadingIndicator()
|
||||
}
|
||||
} else if (apps.isNotEmpty()) {
|
||||
items(
|
||||
apps,
|
||||
key = { "APP-" + it.currentPackageName },
|
||||
contentType = { "APP" },
|
||||
) { installedApp ->
|
||||
val packageInfo = viewModel.packageInfoMap[installedApp.currentPackageName]
|
||||
|
||||
} ?: item { LoadingIndicator() }
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { onAppClick(installedApp) },
|
||||
leadingContent = {
|
||||
AppIcon(
|
||||
packageInfo,
|
||||
contentDescription = null,
|
||||
Modifier.size(36.dp)
|
||||
)
|
||||
},
|
||||
headlineContent = { AppLabel(packageInfo, defaultText = null) },
|
||||
supportingContent = { Text(installedApp.currentPackageName) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item(key = "NONE") {
|
||||
Text(
|
||||
text = stringResource(R.string.no_patched_apps_found),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -19,8 +19,12 @@ import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
@@ -40,16 +44,51 @@ class AppSelectorViewModel(
|
||||
"input.apk"
|
||||
).also(File::delete)
|
||||
}
|
||||
val appList = pm.appList
|
||||
|
||||
val filterTextFlow = MutableStateFlow("")
|
||||
val filterText: StateFlow<String> = filterTextFlow
|
||||
|
||||
val apps = pm.appList.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
val filteredApps = filterText
|
||||
.combine(apps) { filter, apps ->
|
||||
if (apps == null || filter.isBlank()) {
|
||||
apps
|
||||
} else {
|
||||
apps.filter { app ->
|
||||
app.packageName.contains(filter, true) ||
|
||||
loadLabel(app.packageInfo).contains(filter)
|
||||
}
|
||||
}
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(
|
||||
stopTimeoutMillis = 0,
|
||||
replayExpirationMillis = 10_000,
|
||||
),
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
private val storageSelectionChannel = Channel<SelectedApp.Local>()
|
||||
val storageSelectionFlow = storageSelectionChannel.receiveAsFlow()
|
||||
|
||||
val suggestedAppVersions = patchBundleRepository.suggestedVersions.flowOn(Dispatchers.Default)
|
||||
val suggestedAppVersions = patchBundleRepository.suggestedVersions.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(null)
|
||||
private set
|
||||
|
||||
fun setFilterText(filter: String) {
|
||||
filterTextFlow.value = filter
|
||||
}
|
||||
|
||||
fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" }
|
||||
|
||||
fun dismissNonSuggestedVersionDialog() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,16 @@ import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.domain.installer.RootServiceException
|
||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.installer.RootServiceException
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.collectEach
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -20,32 +23,42 @@ class InstalledAppsViewModel(
|
||||
private val pm: PM,
|
||||
private val rootInstaller: RootInstaller
|
||||
) : ViewModel() {
|
||||
val apps = installedAppsRepository.getAll().flowOn(Dispatchers.IO)
|
||||
val apps = installedAppsRepository.getAll()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
val packageInfoMap = mutableStateMapOf<String, PackageInfo?>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
apps.collectEach { installedApp ->
|
||||
packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (
|
||||
installedApp.installType == InstallType.MOUNT && !rootInstaller.isAppInstalled(installedApp.currentPackageName)
|
||||
) {
|
||||
installedAppsRepository.delete(installedApp)
|
||||
return@withContext null
|
||||
}
|
||||
} catch (_: RootServiceException) { }
|
||||
apps.filterNotNull().collectLatest(::fetchPackageInfos)
|
||||
}
|
||||
}
|
||||
|
||||
val packageInfo = pm.getPackageInfo(installedApp.currentPackageName)
|
||||
|
||||
if (packageInfo == null && installedApp.installType != InstallType.MOUNT) {
|
||||
installedAppsRepository.delete(installedApp)
|
||||
private suspend fun fetchPackageInfos(apps: List<InstalledApp>) {
|
||||
for (app in apps) {
|
||||
packageInfoMap[app.currentPackageName] = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (app.installType == InstallType.MOUNT &&
|
||||
!rootInstaller.isAppInstalled(app.currentPackageName)
|
||||
) {
|
||||
installedAppsRepository.delete(app)
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
packageInfo
|
||||
} catch (_: RootServiceException) {
|
||||
}
|
||||
|
||||
val packageInfo = pm.getPackageInfo(app.currentPackageName)
|
||||
|
||||
if (packageInfo == null && app.installType != InstallType.MOUNT) {
|
||||
installedAppsRepository.delete(app)
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
packageInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user