Merge branch 'compose-dev' into compose/downloader-system

This commit is contained in:
Ax333l 2024-11-17 19:21:41 +01:00
commit 3029a61e99
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
73 changed files with 1232 additions and 657 deletions

View File

@ -5,20 +5,20 @@ plugins {
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.devtools) alias(libs.plugins.devtools)
alias(libs.plugins.about.libraries) alias(libs.plugins.about.libraries)
alias(libs.plugins.compose.compiler)
} }
android { android {
namespace = "app.revanced.manager" namespace = "app.revanced.manager"
compileSdk = 34 compileSdk = 35
buildToolsVersion = "34.0.0" buildToolsVersion = "35.0.0"
defaultConfig { defaultConfig {
applicationId = "app.revanced.manager" applicationId = "app.revanced.manager"
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "0.0.1" versionName = "0.0.1"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@ -195,6 +195,10 @@ dependencies {
// Scrollbars // Scrollbars
implementation(libs.scrollbars) implementation(libs.scrollbars)
// EnumUtil
implementation(libs.enumutil)
ksp(libs.enumutil.ksp)
// Reorderable lists // Reorderable lists
implementation(libs.reorderable) implementation(libs.reorderable)

View File

@ -2,11 +2,11 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "393045599eb516fc7ef99f142485e9a2", "identityHash": "167d15a56dd60ffcebf1f93aa6948a93",
"entities": [ "entities": [
{ {
"tableName": "patch_bundles", "tableName": "patch_bundles",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, `version` TEXT, `integrations_version` TEXT, PRIMARY KEY(`uid`))", "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": [ "fields": [
{ {
"fieldPath": "uid", "fieldPath": "uid",
@ -20,6 +20,12 @@
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
}, },
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
},
{ {
"fieldPath": "source", "fieldPath": "source",
"columnName": "source", "columnName": "source",
@ -31,18 +37,6 @@
"columnName": "auto_update", "columnName": "auto_update",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": true
},
{
"fieldPath": "versionInfo.patches",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "versionInfo.integrations",
"columnName": "integrations_version",
"affinity": "TEXT",
"notNull": false
} }
], ],
"primaryKey": { "primaryKey": {
@ -423,7 +417,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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, '393045599eb516fc7ef99f142485e9a2')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '167d15a56dd60ffcebf1f93aa6948a93')"
] ]
} }
} }

View File

@ -9,7 +9,7 @@ import kotlinx.parcelize.Parcelize
enum class InstallType(val stringResource: Int) { enum class InstallType(val stringResource: Int) {
DEFAULT(R.string.default_install), DEFAULT(R.string.default_install),
ROOT(R.string.root_install) MOUNT(R.string.mount_install)
} }
@Parcelize @Parcelize

View File

@ -8,11 +8,11 @@ interface PatchBundleDao {
@Query("SELECT * FROM patch_bundles") @Query("SELECT * FROM patch_bundles")
suspend fun all(): List<PatchBundleEntity> suspend fun all(): List<PatchBundleEntity>
@Query("SELECT version, integrations_version, auto_update FROM patch_bundles WHERE uid = :uid") @Query("SELECT version, auto_update FROM patch_bundles WHERE uid = :uid")
fun getPropsById(uid: Int): Flow<BundleProperties?> fun getPropsById(uid: Int): Flow<BundleProperties?>
@Query("UPDATE patch_bundles SET version = :patches, integrations_version = :integrations WHERE uid = :uid") @Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
suspend fun updateVersion(uid: Int, patches: String?, integrations: String?) suspend fun updateVersion(uid: Int, patches: String?)
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid") @Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
suspend fun setAutoUpdate(uid: Int, value: Boolean) suspend fun setAutoUpdate(uid: Int, value: Boolean)
@ -26,7 +26,7 @@ interface PatchBundleDao {
@Transaction @Transaction
suspend fun reset() { suspend fun reset() {
purgeCustomBundles() purgeCustomBundles()
updateVersion(0, null, null) // Reset the main source updateVersion(0, null) // Reset the main source
} }
@Query("DELETE FROM patch_bundles WHERE uid = :uid") @Query("DELETE FROM patch_bundles WHERE uid = :uid")

View File

@ -29,21 +29,16 @@ sealed class Source {
} }
} }
data class VersionInfo(
@ColumnInfo(name = "version") val patches: String? = null,
@ColumnInfo(name = "integrations_version") val integrations: String? = null,
)
@Entity(tableName = "patch_bundles") @Entity(tableName = "patch_bundles")
data class PatchBundleEntity( data class PatchBundleEntity(
@PrimaryKey val uid: Int, @PrimaryKey val uid: Int,
@ColumnInfo(name = "name") val name: String, @ColumnInfo(name = "name") val name: String,
@Embedded val versionInfo: VersionInfo, @ColumnInfo(name = "version") val version: String? = null,
@ColumnInfo(name = "source") val source: Source, @ColumnInfo(name = "source") val source: Source,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean @ColumnInfo(name = "auto_update") val autoUpdate: Boolean
) )
data class BundleProperties( data class BundleProperties(
@Embedded val versionInfo: VersionInfo, @ColumnInfo(name = "version") val version: String? = null,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean @ColumnInfo(name = "auto_update") val autoUpdate: Boolean
) )

View File

@ -20,6 +20,8 @@ import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long import kotlinx.serialization.json.long
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.typeOf
@Entity( @Entity(
tableName = "options", tableName = "options",
@ -46,8 +48,8 @@ data class Option(
val errorMessage = "Cannot deserialize value as ${option.type}" val errorMessage = "Cannot deserialize value as ${option.type}"
try { try {
if (option.type.endsWith("Array")) { if (option.type.classifier == List::class) {
val elementType = option.type.removeSuffix("Array") val elementType = option.type.arguments.first().type!!
return raw.jsonArray.map { deserializeBasicType(elementType, it.jsonPrimitive) } return raw.jsonArray.map { deserializeBasicType(elementType, it.jsonPrimitive) }
} }
@ -67,12 +69,17 @@ data class Option(
allowSpecialFloatingPointValues = true allowSpecialFloatingPointValues = true
} }
private fun deserializeBasicType(type: String, value: JsonPrimitive) = when (type) { private fun deserializeBasicType(type: KType, value: JsonPrimitive) = when (type) {
"Boolean" -> value.boolean typeOf<Boolean>() -> value.boolean
"Int" -> value.int typeOf<Int>() -> value.int
"Long" -> value.long typeOf<Long>() -> value.long
"Float" -> value.float typeOf<Float>() -> value.float
"String" -> value.content.also { if (!value.isString) throw SerializationException("Expected value to be a string: $value") } typeOf<String>() -> value.content.also {
if (!value.isString) throw SerializationException(
"Expected value to be a string: $value"
)
}
else -> throw SerializationException("Unknown type: $type") else -> throw SerializationException("Unknown type: $type")
} }

View File

@ -4,29 +4,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.nio.file.Files
import java.nio.file.StandardCopyOption
class LocalPatchBundle(name: String, id: Int, directory: File) : class LocalPatchBundle(name: String, id: Int, directory: File) :
PatchBundleSource(name, id, directory) { PatchBundleSource(name, id, directory) {
suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) { suspend fun replace(patches: InputStream) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
patches?.let { inputStream -> patchBundleOutputStream().use { outputStream ->
patchBundleOutputStream().use { outputStream -> patches.copyTo(outputStream)
inputStream.copyTo(outputStream)
}
}
integrations?.let {
Files.copy(
it,
this@LocalPatchBundle.integrationsFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
} }
} }
reload()?.also { reload()?.also {
saveVersion(it.readManifestAttribute("Version"), null) saveVersion(it.readManifestAttribute("Version"))
} }
} }
} }

View File

@ -28,7 +28,6 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
protected val configRepository: PatchBundlePersistenceRepository by inject() protected val configRepository: PatchBundlePersistenceRepository by inject()
private val app: Application by inject() private val app: Application by inject()
protected val patchesFile = directory.resolve("patches.jar") protected val patchesFile = directory.resolve("patches.jar")
protected val integrationsFile = directory.resolve("integrations.apk")
private val _state = MutableStateFlow(load()) private val _state = MutableStateFlow(load())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -58,7 +57,7 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
if (!hasInstalled()) return State.Missing if (!hasInstalled()) return State.Missing
return try { return try {
State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists))) State.Loaded(PatchBundle(patchesFile))
} catch (t: Throwable) { } catch (t: Throwable) {
Log.e(tag, "Failed to load patch bundle with UID $uid", t) Log.e(tag, "Failed to load patch bundle with UID $uid", t)
State.Failed(t) State.Failed(t)
@ -85,9 +84,9 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default) fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
suspend fun getProps() = propsFlow().first()!! suspend fun getProps() = propsFlow().first()!!
suspend fun currentVersion() = getProps().versionInfo suspend fun currentVersion() = getProps().version
protected suspend fun saveVersion(patches: String?, integrations: String?) = protected suspend fun saveVersion(version: String?) =
configRepository.updateVersion(uid, patches, integrations) configRepository.updateVersion(uid, version)
suspend fun setName(name: String) { suspend fun setName(name: String) {
configRepository.setName(uid, name) configRepository.setName(uid, name)

View File

@ -1,20 +1,12 @@
package app.revanced.manager.domain.bundles package app.revanced.manager.domain.bundles
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import app.revanced.manager.data.room.bundles.VersionInfo
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType import app.revanced.manager.network.dto.PatchBundleInfo
import app.revanced.manager.network.dto.BundleAsset
import app.revanced.manager.network.dto.BundleInfo
import app.revanced.manager.network.service.HttpService import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.utils.getOrThrow import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.JAR_MIMETYPE
import io.ktor.client.request.url import io.ktor.client.request.url
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
@ -24,27 +16,17 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
PatchBundleSource(name, id, directory) { PatchBundleSource(name, id, directory) {
protected val http: HttpService by inject() protected val http: HttpService by inject()
protected abstract suspend fun getLatestInfo(): BundleInfo protected abstract suspend fun getLatestInfo(): PatchBundleInfo
private suspend fun download(info: BundleInfo) = withContext(Dispatchers.IO) { private suspend fun download(info: PatchBundleInfo) = withContext(Dispatchers.IO) {
val (patches, integrations) = info val (version, url) = info
coroutineScope { patchBundleOutputStream().use {
launch { http.streamTo(it) {
patchBundleOutputStream().use { url(url)
http.streamTo(it) {
url(patches.url)
}
}
}
launch {
http.download(integrationsFile) {
url(integrations.url)
}
} }
} }
saveVersion(patches.version, integrations.version) saveVersion(version)
reload() reload()
} }
@ -54,20 +36,15 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
suspend fun update(): Boolean = withContext(Dispatchers.IO) { suspend fun update(): Boolean = withContext(Dispatchers.IO) {
val info = getLatestInfo() val info = getLatestInfo()
if (hasInstalled() && VersionInfo( if (hasInstalled() && info.version == currentVersion())
info.patches.version,
info.integrations.version
) == currentVersion()
) {
return@withContext false return@withContext false
}
download(info) download(info)
true true
} }
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) { suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
arrayOf(patchesFile, integrationsFile).forEach(File::delete) patchesFile.delete()
reload() reload()
} }
@ -81,7 +58,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) : class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle(name, id, directory, endpoint) { RemotePatchBundle(name, id, directory, endpoint) {
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) { override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
http.request<BundleInfo> { http.request<PatchBundleInfo> {
url(endpoint) url(endpoint)
}.getOrThrow() }.getOrThrow()
} }
@ -91,22 +68,10 @@ class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
RemotePatchBundle(name, id, directory, endpoint) { RemotePatchBundle(name, id, directory, endpoint) {
private val api: ReVancedAPI by inject() private val api: ReVancedAPI by inject()
override suspend fun getLatestInfo() = coroutineScope { override suspend fun getLatestInfo() = api
fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) { .getLatestRelease("revanced-patches")
api .getOrThrow()
.getLatestRelease(repo) .let {
.getOrThrow() PatchBundleInfo(it.version, it.assets.first { it.name.endsWith(".rvp") }.downloadUrl)
.let {
BundleAsset(it.version, it.findAssetByType(mime).downloadUrl)
}
} }
val patches = getAssetAsync("revanced-patches", JAR_MIMETYPE)
val integrations = getAssetAsync("revanced-integrations", APK_MIMETYPE)
BundleInfo(
patches.await(),
integrations.await()
)
}
} }

View File

@ -43,7 +43,7 @@ class RootInstaller(
} }
} }
return withTimeoutOrNull(Duration.ofSeconds(120L)) { return withTimeoutOrNull(Duration.ofSeconds(20L)) {
remoteFS.await() remoteFS.await()
} ?: throw RootServiceException() } ?: throw RootServiceException()
} }
@ -58,6 +58,10 @@ class RootInstaller(
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
fun isDeviceRooted() = System.getenv("PATH")?.split(":")?.any { path ->
File(path, "su").canExecute()
} ?: false
suspend fun isAppInstalled(packageName: String) = suspend fun isAppInstalled(packageName: String) =
awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists() awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists()
@ -105,7 +109,12 @@ class RootInstaller(
stockAPK?.let { stockApp -> stockAPK?.let { stockApp ->
pm.getPackageInfo(packageName)?.let { packageInfo -> pm.getPackageInfo(packageName)?.let { packageInfo ->
if (packageInfo.versionName <= version) // TODO: get user id programmatically
if (pm.getVersionCode(packageInfo) <= pm.getVersionCode(
pm.getPackageInfo(patchedAPK)
?: error("Failed to get package info for patched app")
)
)
execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app") execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app")
} }

View File

@ -12,6 +12,8 @@ import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.file.Files import java.nio.file.Files
import java.security.UnrecoverableKeyException import java.security.UnrecoverableKeyException
import java.util.Date
import kotlin.time.Duration.Companion.days
class KeystoreManager(app: Application, private val prefs: PreferencesManager) { class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
companion object Constants { companion object Constants {
@ -19,6 +21,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
* Default alias and password for the keystore. * Default alias and password for the keystore.
*/ */
const val DEFAULT = "ReVanced" const val DEFAULT = "ReVanced"
private val eightYearsFromNow get() = Date(System.currentTimeMillis() + (365.days * 8).inWholeMilliseconds * 24)
} }
private val keystorePath = private val keystorePath =
@ -29,23 +32,26 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
prefs.keystorePass.value = pass prefs.keystorePass.value = pass
} }
private suspend fun signingOptions(path: File = keystorePath) = ApkUtils.SigningOptions( private suspend fun signingDetails(path: File = keystorePath) = ApkUtils.KeyStoreDetails(
keyStore = path, keyStore = path,
keyStorePassword = null, keyStorePassword = null,
alias = prefs.keystoreCommonName.get(), alias = prefs.keystoreCommonName.get(),
signer = prefs.keystoreCommonName.get(),
password = prefs.keystorePass.get() password = prefs.keystorePass.get()
) )
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) { suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
ApkUtils.sign(input, output, signingOptions()) ApkUtils.signApk(input, output, prefs.keystoreCommonName.get(), signingDetails())
} }
suspend fun regenerate() = withContext(Dispatchers.Default) { suspend fun regenerate() = withContext(Dispatchers.Default) {
val keyCertPair = ApkSigner.newPrivateKeyCertificatePair(
prefs.keystoreCommonName.get(),
eightYearsFromNow
)
val ks = ApkSigner.newKeyStore( val ks = ApkSigner.newKeyStore(
setOf( setOf(
ApkSigner.KeyStoreEntry( ApkSigner.KeyStoreEntry(
DEFAULT, DEFAULT DEFAULT, DEFAULT, keyCertPair
) )
) )
) )
@ -64,7 +70,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
try { try {
val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null) val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null)
ApkSigner.readKeyCertificatePair(ks, cn, pass) ApkSigner.readPrivateKeyCertificatePair(ks, cn, pass)
} catch (_: UnrecoverableKeyException) { } catch (_: UnrecoverableKeyException) {
return false return false
} catch (_: IllegalArgumentException) { } catch (_: IllegalArgumentException) {

View File

@ -12,7 +12,6 @@ class PreferencesManager(
val api = stringPreference("api_url", "https://api.revanced.app") val api = stringPreference("api_url", "https://api.revanced.app")
val multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true)
val useProcessRuntime = booleanPreference("use_process_runtime", false) val useProcessRuntime = booleanPreference("use_process_runtime", false)
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700) val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700)

View File

@ -102,7 +102,7 @@ class DownloadedAppRepository(private val app: Application, db: AppDatabase, pri
dao.insert( dao.insert(
DownloadedApp( DownloadedApp(
packageName = pkgInfo.packageName, packageName = pkgInfo.packageName,
version = pkgInfo.versionName, version = pkgInfo.versionName!!,
directory = relativePath, directory = relativePath,
) )
) )

View File

@ -87,11 +87,11 @@ class DownloaderPluginRepository(
return try { return try {
val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!! val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!!
val className = packageInfo.applicationInfo.metaData.getString(METADATA_PLUGIN_CLASS) val className = packageInfo.applicationInfo!!.metaData.getString(METADATA_PLUGIN_CLASS)
?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS") ?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS")
val classLoader = val classLoader =
PathClassLoader(packageInfo.applicationInfo.sourceDir, app.classLoader) PathClassLoader(packageInfo.applicationInfo!!.sourceDir, app.classLoader)
val pluginContext = app.createPackageContext(packageName, 0) val pluginContext = app.createPackageContext(packageName, 0)
val downloader = classLoader val downloader = classLoader
@ -109,7 +109,7 @@ class DownloaderPluginRepository(
LoadedDownloaderPlugin( LoadedDownloaderPlugin(
packageName, packageName,
with(pm) { packageInfo.label() }, with(pm) { packageInfo.label() },
packageInfo.versionName, packageInfo.versionName!!,
downloader.get, downloader.get,
downloader.download, downloader.download,
classLoader classLoader

View File

@ -4,7 +4,6 @@ import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.bundles.PatchBundleEntity import app.revanced.manager.data.room.bundles.PatchBundleEntity
import app.revanced.manager.data.room.bundles.Source import app.revanced.manager.data.room.bundles.Source
import app.revanced.manager.data.room.bundles.VersionInfo
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
class PatchBundlePersistenceRepository(db: AppDatabase) { class PatchBundlePersistenceRepository(db: AppDatabase) {
@ -26,7 +25,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
PatchBundleEntity( PatchBundleEntity(
uid = generateUid(), uid = generateUid(),
name = name, name = name,
versionInfo = VersionInfo(), version = null,
source = source, source = source,
autoUpdate = autoUpdate autoUpdate = autoUpdate
).also { ).also {
@ -35,8 +34,8 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
suspend fun delete(uid: Int) = dao.remove(uid) suspend fun delete(uid: Int) = dao.remove(uid)
suspend fun updateVersion(uid: Int, patches: String?, integrations: String?) = suspend fun updateVersion(uid: Int, version: String?) =
dao.updateVersion(uid, patches, integrations) dao.updateVersion(uid, version)
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value) suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
@ -48,7 +47,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
val defaultSource = PatchBundleEntity( val defaultSource = PatchBundleEntity(
uid = 0, uid = 0,
name = "", name = "",
versionInfo = VersionInfo(), version = null,
source = Source.API, source = Source.API,
autoUpdate = false autoUpdate = false
) )

View File

@ -3,7 +3,7 @@ package app.revanced.manager.domain.repository
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import app.revanced.library.PatchUtils import app.revanced.library.mostCommonCompatibleVersions
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.data.room.bundles.PatchBundleEntity import app.revanced.manager.data.room.bundles.PatchBundleEntity
@ -55,7 +55,7 @@ class PatchBundleRepository(
val allPatches = val allPatches =
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet() it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
PatchUtils.getMostCommonCompatibleVersions(allPatches, countUnusedPatches = true) allPatches.mostCommonCompatibleVersions(countUnusedPatches = true)
.mapValues { (_, versions) -> .mapValues { (_, versions) ->
if (versions.keys.size < 2) if (versions.keys.size < 2)
return@mapValues versions.keys.firstOrNull() return@mapValues versions.keys.firstOrNull()
@ -137,11 +137,11 @@ class PatchBundleRepository(
private fun addBundle(patchBundle: PatchBundleSource) = private fun addBundle(patchBundle: PatchBundleSource) =
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } } _sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }
suspend fun createLocal(patches: InputStream, integrations: InputStream?) = withContext(Dispatchers.Default) { suspend fun createLocal(patches: InputStream) = withContext(Dispatchers.Default) {
val uid = persistenceRepo.create("", SourceInfo.Local).uid val uid = persistenceRepo.create("", SourceInfo.Local).uid
val bundle = LocalPatchBundle("", uid, directoryOf(uid)) val bundle = LocalPatchBundle("", uid, directoryOf(uid))
bundle.replace(patches, integrations) bundle.replace(patches)
addBundle(bundle) addBundle(bundle)
} }

View File

@ -1,9 +0,0 @@
package app.revanced.manager.network.dto
import kotlinx.serialization.Serializable
@Serializable
data class BundleInfo(val patches: BundleAsset, val integrations: BundleAsset)
@Serializable
data class BundleAsset(val version: String, val url: String)

View File

@ -0,0 +1,7 @@
package app.revanced.manager.network.dto
import kotlinx.serialization.Serializable
@Serializable
// TODO: replace this
data class PatchBundleInfo(val version: String, val url: String)

View File

@ -22,7 +22,6 @@ class Session(
cacheDir: String, cacheDir: String,
frameworkDir: String, frameworkDir: String,
aaptPath: String, aaptPath: String,
multithreadingDexFileWriter: Boolean,
private val androidContext: Context, private val androidContext: Context,
private val logger: Logger, private val logger: Logger,
private val input: File, private val input: File,
@ -38,8 +37,7 @@ class Session(
apkFile = input, apkFile = input,
temporaryFilesPath = tempDir, temporaryFilesPath = tempDir,
frameworkFileDirectory = frameworkDir, frameworkFileDirectory = frameworkDir,
aaptBinaryPath = aaptPath, aaptBinaryPath = aaptPath
multithreadingDexFileWriter = multithreadingDexFileWriter,
) )
) )
@ -51,7 +49,7 @@ class Session(
state = State.RUNNING state = State.RUNNING
) )
this.apply(true).collect { (patch, exception) -> this().collect { (patch, exception) ->
if (patch !in selectedPatches) return@collect if (patch !in selectedPatches) return@collect
if (exception != null) { if (exception != null) {
@ -89,7 +87,7 @@ class Session(
) )
} }
suspend fun run(output: File, selectedPatches: PatchList, integrations: List<File>) { suspend fun run(output: File, selectedPatches: PatchList) {
updateProgress(state = State.COMPLETED) // Unpacking updateProgress(state = State.COMPLETED) // Unpacking
java.util.logging.Logger.getLogger("").apply { java.util.logging.Logger.getLogger("").apply {
@ -103,8 +101,7 @@ class Session(
with(patcher) { with(patcher) {
logger.info("Merging integrations") logger.info("Merging integrations")
acceptIntegrations(integrations.toSet()) this += selectedPatches.toSet()
acceptPatches(selectedPatches.toSet())
logger.info("Applying patches...") logger.info("Applying patches...")
applyPatchesVerbose(selectedPatches.sortedBy { it.name }) applyPatchesVerbose(selectedPatches.sortedBy { it.name })

View File

@ -2,17 +2,17 @@ package app.revanced.manager.patcher.patch
import android.util.Log import android.util.Log
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchLoader
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.jar.JarFile import java.util.jar.JarFile
class PatchBundle(val patchesJar: File, val integrations: File?) { class PatchBundle(val patchesJar: File) {
private val loader = object : Iterable<Patch<*>> { private val loader = object : Iterable<Patch<*>> {
private fun load(): Iterable<Patch<*>> { private fun load(): Iterable<Patch<*>> {
patchesJar.setReadOnly() patchesJar.setReadOnly()
return PatchBundleLoader.Dex(patchesJar, optimizedDexDirectory = null) return PatchLoader.Dex(setOf(patchesJar))
} }
override fun iterator(): Iterator<Patch<*>> = load().iterator() override fun iterator(): Iterator<Patch<*>> = load().iterator()
@ -41,12 +41,12 @@ class PatchBundle(val patchesJar: File, val integrations: File?) {
/** /**
* Load all patches compatible with the specified package. * Load all patches compatible with the specified package.
*/ */
fun patchClasses(packageName: String) = loader.filter { patch -> fun patches(packageName: String) = loader.filter { patch ->
val compatiblePackages = patch.compatiblePackages val compatiblePackages = patch.compatiblePackages
?: // The patch has no compatibility constraints, which means it is universal. ?: // The patch has no compatibility constraints, which means it is universal.
return@filter true return@filter true
if (!compatiblePackages.any { it.name == packageName }) { if (!compatiblePackages.any { (name, _) -> name == packageName }) {
// Patch is not compatible with this package. // Patch is not compatible with this package.
return@filter false return@filter false
} }

View File

@ -1,14 +1,14 @@
package app.revanced.manager.patcher.patch package app.revanced.manager.patcher.patch
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.ResourcePatch import app.revanced.patcher.patch.Option as PatchOption
import app.revanced.patcher.patch.options.PatchOption import app.revanced.patcher.patch.resourcePatch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlin.reflect.KType
data class PatchInfo( data class PatchInfo(
val name: String, val name: String,
@ -21,7 +21,12 @@ data class PatchInfo(
patch.name.orEmpty(), patch.name.orEmpty(),
patch.description, patch.description,
patch.use, patch.use,
patch.compatiblePackages?.map { CompatiblePackage(it) }?.toImmutableList(), patch.compatiblePackages?.map { (pkgName, versions) ->
CompatiblePackage(
pkgName,
versions?.toImmutableSet()
)
}?.toImmutableList(),
patch.options.map { (_, option) -> Option(option) }.ifEmpty { null }?.toImmutableList() patch.options.map { (_, option) -> Option(option) }.ifEmpty { null }?.toImmutableList()
) )
@ -44,37 +49,19 @@ data class PatchInfo(
* The resulting patch cannot be executed. * The resulting patch cannot be executed.
* This is necessary because some functions in ReVanced Library only accept full [Patch] objects. * This is necessary because some functions in ReVanced Library only accept full [Patch] objects.
*/ */
fun toPatcherPatch(): Patch<*> = object : ResourcePatch( fun toPatcherPatch(): Patch<*> =
name = name, resourcePatch(name = name, description = description, use = include) {
description = description, compatiblePackages?.let { pkgs ->
compatiblePackages = compatiblePackages compatibleWith(*pkgs.map { it.packageName to it.versions }.toTypedArray())
?.map(app.revanced.manager.patcher.patch.CompatiblePackage::toPatcherCompatiblePackage) }
?.toSet(), }
use = include,
) {
override fun execute(context: ResourceContext) =
throw Exception("Metadata patches cannot be executed")
}
} }
@Immutable @Immutable
data class CompatiblePackage( data class CompatiblePackage(
val packageName: String, val packageName: String,
val versions: ImmutableSet<String>? val versions: ImmutableSet<String>?
) { )
constructor(pkg: Patch.CompatiblePackage) : this(
pkg.name,
pkg.versions?.toImmutableSet()
)
/**
* Converts this [CompatiblePackage] into a [Patch.CompatiblePackage] from patcher.
*/
fun toPatcherCompatiblePackage() = Patch.CompatiblePackage(
name = packageName,
versions = versions,
)
}
@Immutable @Immutable
data class Option<T>( data class Option<T>(
@ -82,7 +69,7 @@ data class Option<T>(
val key: String, val key: String,
val description: String, val description: String,
val required: Boolean, val required: Boolean,
val type: String, val type: KType,
val default: T?, val default: T?,
val presets: Map<String, T?>?, val presets: Map<String, T?>?,
val validator: (T?) -> Boolean, val validator: (T?) -> Boolean,
@ -92,7 +79,7 @@ data class Option<T>(
option.key, option.key,
option.description.orEmpty(), option.description.orEmpty(),
option.required, option.required,
option.valueType, option.type,
option.default, option.default,
option.values, option.values,
{ option.validator(option, it) }, { option.validator(option, it) },

View File

@ -27,15 +27,13 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
val selectedBundles = selectedPatches.keys val selectedBundles = selectedPatches.keys
val allPatches = bundles.filterKeys { selectedBundles.contains(it) } val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
.mapValues { (_, bundle) -> bundle.patchClasses(packageName) } .mapValues { (_, bundle) -> bundle.patches(packageName) }
val patchList = selectedPatches.flatMap { (bundle, selected) -> val patchList = selectedPatches.flatMap { (bundle, selected) ->
allPatches[bundle]?.filter { selected.contains(it.name) } allPatches[bundle]?.filter { selected.contains(it.name) }
?: throw IllegalArgumentException("Patch bundle $bundle does not exist") ?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
} }
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
// Set all patch options. // Set all patch options.
options.forEach { (bundle, bundlePatchOptions) -> options.forEach { (bundle, bundlePatchOptions) ->
val patches = allPatches[bundle] ?: return@forEach val patches = allPatches[bundle] ?: return@forEach
@ -53,7 +51,6 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
cacheDir, cacheDir,
frameworkPath, frameworkPath,
aaptPath, aaptPath,
enableMultithreadedDexWriter(),
context, context,
logger, logger,
File(inputFile), File(inputFile),
@ -62,8 +59,7 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
).use { session -> ).use { session ->
session.run( session.run(
File(outputFile), File(outputFile),
patchList, patchList
integrations
) )
} }
} }

View File

@ -70,7 +70,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
onProgress: ProgressEventHandler, onProgress: ProgressEventHandler,
) = coroutineScope { ) = coroutineScope {
// Get the location of our own Apk. // Get the location of our own Apk.
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo.sourceDir val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
val limit = "${prefs.patcherProcessMemoryLimit.get()}M" val limit = "${prefs.patcherProcessMemoryLimit.get()}M"
val propOverride = resolvePropOverride(context)?.absolutePath val propOverride = resolvePropOverride(context)?.absolutePath
@ -148,13 +148,11 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
packageName = packageName, packageName = packageName,
inputFile = inputFile, inputFile = inputFile,
outputFile = outputFile, outputFile = outputFile,
enableMultithrededDexWriter = enableMultithreadedDexWriter(),
configurations = selectedPatches.map { (id, patches) -> configurations = selectedPatches.map { (id, patches) ->
val bundle = bundles[id]!! val bundle = bundles[id]!!
PatchConfiguration( PatchConfiguration(
bundle.patchesJar.absolutePath, bundle.patchesJar.absolutePath,
bundle.integrations?.absolutePath,
patches, patches,
options[id].orEmpty() options[id].orEmpty()
) )

View File

@ -26,7 +26,6 @@ sealed class Runtime(context: Context) : KoinComponent {
context.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath context.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
protected suspend fun bundles() = patchBundlesRepo.bundles.first() protected suspend fun bundles() = patchBundlesRepo.bundles.first()
protected suspend fun enableMultithreadedDexWriter() = prefs.multithreadingDexFileWriter.get()
abstract suspend fun execute( abstract suspend fun execute(
inputFile: String, inputFile: String,

View File

@ -12,14 +12,12 @@ data class Parameters(
val packageName: String, val packageName: String,
val inputFile: String, val inputFile: String,
val outputFile: String, val outputFile: String,
val enableMultithrededDexWriter: Boolean,
val configurations: List<PatchConfiguration>, val configurations: List<PatchConfiguration>,
) : Parcelable ) : Parcelable
@Parcelize @Parcelize
data class PatchConfiguration( data class PatchConfiguration(
val bundlePath: String, val bundlePath: String,
val integrationsPath: String?,
val patches: Set<String>, val patches: Set<String>,
val options: @RawValue Map<String, Map<String, Any?>> val options: @RawValue Map<String, Map<String, Any?>>
) : Parcelable ) : Parcelable

View File

@ -54,13 +54,11 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB") logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
val integrations =
parameters.configurations.mapNotNull { it.integrationsPath?.let(::File) }
val patchList = parameters.configurations.flatMap { config -> val patchList = parameters.configurations.flatMap { config ->
val bundle = PatchBundle(File(config.bundlePath), null) val bundle = PatchBundle(File(config.bundlePath))
val patches = val patches =
bundle.patchClasses(parameters.packageName).filter { it.name in config.patches } bundle.patches(parameters.packageName).filter { it.name in config.patches }
.associateBy { it.name } .associateBy { it.name }
config.options.forEach { (patchName, opts) -> config.options.forEach { (patchName, opts) ->
@ -81,7 +79,6 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
cacheDir = parameters.cacheDir, cacheDir = parameters.cacheDir,
aaptPath = parameters.aaptPath, aaptPath = parameters.aaptPath,
frameworkDir = parameters.frameworkDir, frameworkDir = parameters.frameworkDir,
multithreadingDexFileWriter = parameters.enableMultithrededDexWriter,
androidContext = context, androidContext = context,
logger = logger, logger = logger,
input = File(parameters.inputFile), input = File(parameters.inputFile),
@ -90,7 +87,7 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
events.progress(name, state?.name, message) events.progress(name, state?.name, message)
} }
).use { ).use {
it.run(File(parameters.outputFile), patchList, integrations) it.run(File(parameters.outputFile), patchList)
} }
events.finished(null) events.finished(null)

View File

@ -149,7 +149,7 @@ class PatcherWorker(
return try { return try {
if (args.input is SelectedApp.Installed) { if (args.input is SelectedApp.Installed) {
installedAppRepository.get(args.packageName)?.let { installedAppRepository.get(args.packageName)?.let {
if (it.installType == InstallType.ROOT) { if (it.installType == InstallType.MOUNT) {
rootInstaller.unmount(args.packageName) rootInstaller.unmount(args.packageName)
} }
} }
@ -209,7 +209,7 @@ class PatcherWorker(
} }
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) } is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo.sourceDir) is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo!!.sourceDir)
} }
val runtime = if (prefs.useProcessRuntime.get()) { val runtime = if (prefs.useProcessRuntime.get()) {

View File

@ -8,7 +8,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Source import androidx.compose.material.icons.outlined.Source
import androidx.compose.material.icons.outlined.Update import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
@ -24,6 +23,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.util.transparentListItemColors import app.revanced.manager.util.transparentListItemColors
@Composable @Composable
@ -77,7 +77,7 @@ private fun AutoUpdatesItem(
) = ListItem( ) = ListItem(
leadingContent = { Icon(icon, null) }, leadingContent = { Icon(icon, null) },
headlineContent = { Text(stringResource(headline)) }, headlineContent = { Text(stringResource(headline)) },
trailingContent = { Checkbox(checked = checked, onCheckedChange = null) }, trailingContent = { HapticCheckbox(checked = checked, onCheckedChange = null) },
modifier = Modifier.clickable { onCheckedChange(!checked) }, modifier = Modifier.clickable { onCheckedChange(!checked) },
colors = transparentListItemColors colors = transparentListItemColors
) )

View File

@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.haptics.HapticCheckbox
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -70,7 +71,7 @@ fun AvailableUpdateDialog(
}, },
leadingContent = { leadingContent = {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
Checkbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it }) HapticCheckbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it })
} }
} }
) )

View File

@ -0,0 +1,159 @@
package app.revanced.manager.ui.component
import android.content.pm.PackageInstaller
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import com.github.materiiapps.enumutil.FromValue
private typealias InstallerStatusDialogButtonHandler = ((model: InstallerModel) -> Unit)
private typealias InstallerStatusDialogButton = @Composable (model: InstallerStatusDialogModel) -> Unit
interface InstallerModel {
fun reinstall()
fun install()
}
interface InstallerStatusDialogModel : InstallerModel {
var packageInstallerStatus: Int?
}
@Composable
fun InstallerStatusDialog(model: InstallerStatusDialogModel) {
val dialogKind = remember {
DialogKind.fromValue(model.packageInstallerStatus!!) ?: DialogKind.FAILURE
}
AlertDialog(
onDismissRequest = {
model.packageInstallerStatus = null
},
confirmButton = {
dialogKind.confirmButton(model)
},
dismissButton = {
dialogKind.dismissButton?.invoke(model)
},
icon = {
Icon(dialogKind.icon, null)
},
title = {
Text(
text = stringResource(dialogKind.title),
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
color = MaterialTheme.colorScheme.onSurface,
)
},
text = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(stringResource(dialogKind.contentStringResId))
}
}
)
}
private fun installerStatusDialogButton(
@StringRes buttonStringResId: Int,
buttonHandler: InstallerStatusDialogButtonHandler = { },
): InstallerStatusDialogButton = { model ->
TextButton(
onClick = {
model.packageInstallerStatus = null
buttonHandler(model)
}
) {
Text(stringResource(buttonStringResId))
}
}
@FromValue("flag")
enum class DialogKind(
val flag: Int,
val title: Int,
@StringRes val contentStringResId: Int,
val icon: ImageVector = Icons.Outlined.ErrorOutline,
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
val dismissButton: InstallerStatusDialogButton? = null,
) {
FAILURE(
flag = PackageInstaller.STATUS_FAILURE,
title = R.string.installation_failed_dialog_title,
contentStringResId = R.string.installation_failed_description,
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
model.install()
}
),
FAILURE_ABORTED(
flag = PackageInstaller.STATUS_FAILURE_ABORTED,
title = R.string.installation_cancelled_dialog_title,
contentStringResId = R.string.installation_aborted_description,
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
model.install()
}
),
FAILURE_BLOCKED(
flag = PackageInstaller.STATUS_FAILURE_BLOCKED,
title = R.string.installation_blocked_dialog_title,
contentStringResId = R.string.installation_blocked_description,
),
FAILURE_CONFLICT(
flag = PackageInstaller.STATUS_FAILURE_CONFLICT,
title = R.string.installation_conflict_dialog_title,
contentStringResId = R.string.installation_conflict_description,
confirmButton = installerStatusDialogButton(R.string.reinstall) { model ->
model.reinstall()
},
dismissButton = installerStatusDialogButton(R.string.cancel),
),
FAILURE_INCOMPATIBLE(
flag = PackageInstaller.STATUS_FAILURE_INCOMPATIBLE,
title = R.string.installation_incompatible_dialog_title,
contentStringResId = R.string.installation_incompatible_description,
),
FAILURE_INVALID(
flag = PackageInstaller.STATUS_FAILURE_INVALID,
title = R.string.installation_invalid_dialog_title,
contentStringResId = R.string.installation_invalid_description,
confirmButton = installerStatusDialogButton(R.string.reinstall) { model ->
model.reinstall()
},
dismissButton = installerStatusDialogButton(R.string.cancel),
),
FAILURE_STORAGE(
flag = PackageInstaller.STATUS_FAILURE_STORAGE,
title = R.string.installation_storage_issue_dialog_title,
contentStringResId = R.string.installation_storage_issue_description,
),
@RequiresApi(34)
FAILURE_TIMEOUT(
flag = PackageInstaller.STATUS_FAILURE_TIMEOUT,
title = R.string.installation_timeout_dialog_title,
contentStringResId = R.string.installation_timeout_description,
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
model.install()
},
);
// Needed due to the @FromValue annotation.
companion object
}

View File

@ -2,37 +2,34 @@ package app.revanced.manager.ui.component.bundle
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material3.FilledTonalButton import androidx.compose.material.icons.outlined.Extension
import androidx.compose.material3.Icon import androidx.compose.material.icons.outlined.Inventory2
import androidx.compose.material3.MaterialTheme import androidx.compose.material.icons.outlined.Sell
import androidx.compose.material3.Switch import androidx.compose.material3.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.TextInputDialog import app.revanced.manager.ui.component.TextInputDialog
import app.revanced.manager.util.isDebuggable import app.revanced.manager.ui.component.haptics.HapticSwitch
@Composable @Composable
fun BaseBundleDialog( fun BaseBundleDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isDefault: Boolean, isDefault: Boolean,
name: String?, name: String?,
onNameChange: ((String) -> Unit)? = null,
remoteUrl: String?, remoteUrl: String?,
onRemoteUrlChange: ((String) -> Unit)? = null, onRemoteUrlChange: ((String) -> Unit)? = null,
patchCount: Int, patchCount: Int,
@ -40,39 +37,66 @@ fun BaseBundleDialog(
autoUpdate: Boolean, autoUpdate: Boolean,
onAutoUpdateChange: (Boolean) -> Unit, onAutoUpdateChange: (Boolean) -> Unit,
onPatchesClick: () -> Unit, onPatchesClick: () -> Unit,
onBundleTypeClick: () -> Unit = {},
extraFields: @Composable ColumnScope.() -> Unit = {} extraFields: @Composable ColumnScope.() -> Unit = {}
) { ) {
ColumnWithScrollbar( ColumnWithScrollbar(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.then(modifier) .then(modifier),
) { ) {
if (name != null) { Column(
var showNameInputDialog by rememberSaveable { modifier = Modifier.padding(16.dp),
mutableStateOf(false) verticalArrangement = Arrangement.spacedBy(4.dp)
} ) {
if (showNameInputDialog) { Row(
TextInputDialog( modifier = Modifier.fillMaxWidth(),
initial = name, horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
title = stringResource(R.string.bundle_input_name), verticalAlignment = Alignment.CenterVertically
onDismissRequest = { ) {
showNameInputDialog = false Icon(
}, imageVector = Icons.Outlined.Inventory2,
onConfirm = { contentDescription = null,
showNameInputDialog = false tint = MaterialTheme.colorScheme.primary,
onNameChange?.invoke(it) modifier = Modifier.size(32.dp)
},
validator = {
it.length in 1..19
}
) )
name?.let {
Text(
text = it,
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
color = MaterialTheme.colorScheme.primary,
)
}
} }
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxWidth()
.padding(start = 2.dp)
) {
version?.let {
Tag(Icons.Outlined.Sell, it)
}
Tag(Icons.Outlined.Extension, patchCount.toString())
}
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
if (remoteUrl != null) {
BundleListItem( BundleListItem(
headlineText = stringResource(R.string.bundle_input_name), headlineText = stringResource(R.string.bundle_auto_update),
supportingText = name.ifEmpty { stringResource(R.string.field_not_set) }, supportingText = stringResource(R.string.bundle_auto_update_description),
modifier = Modifier.clickable(enabled = onNameChange != null) { trailingContent = {
showNameInputDialog = true HapticSwitch(
checked = autoUpdate,
onCheckedChange = onAutoUpdateChange
)
},
modifier = Modifier.clickable {
onAutoUpdateChange(!autoUpdate)
} }
) )
} }
@ -99,81 +123,59 @@ fun BaseBundleDialog(
} }
BundleListItem( BundleListItem(
modifier = Modifier.clickable(enabled = onRemoteUrlChange != null) { modifier = Modifier.clickable(
showUrlInputDialog = true enabled = onRemoteUrlChange != null,
}, onClick = {
headlineText = stringResource(R.string.bundle_input_source_url), showUrlInputDialog = true
supportingText = url.ifEmpty { stringResource(R.string.field_not_set) }
)
}
extraFields()
if (remoteUrl != null) {
BundleListItem(
headlineText = stringResource(R.string.bundle_auto_update),
supportingText = stringResource(R.string.bundle_auto_update_description),
trailingContent = {
Switch(
checked = autoUpdate,
onCheckedChange = onAutoUpdateChange
)
},
modifier = Modifier.clickable {
onAutoUpdateChange(!autoUpdate)
}
)
}
BundleListItem(
headlineText = stringResource(R.string.bundle_type),
supportingText = stringResource(R.string.bundle_type_description),
modifier = Modifier.clickable {
onBundleTypeClick()
}
) {
FilledTonalButton(
onClick = onBundleTypeClick,
content = {
if (remoteUrl == null) {
Text(stringResource(R.string.local))
} else {
Text(stringResource(R.string.remote))
} }
),
headlineText = stringResource(R.string.bundle_input_source_url),
supportingText = url.ifEmpty {
stringResource(R.string.field_not_set)
} }
) )
} }
if (version != null || patchCount > 0) { val patchesClickable = patchCount > 0
Text(
text = stringResource(R.string.information),
modifier = Modifier.padding(
horizontal = 16.dp,
vertical = 12.dp
),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
)
}
val patchesClickable = LocalContext.current.isDebuggable && patchCount > 0
BundleListItem( BundleListItem(
headlineText = stringResource(R.string.patches), headlineText = stringResource(R.string.patches),
supportingText = pluralStringResource(R.plurals.bundle_patches_available, patchCount, patchCount), supportingText = stringResource(R.string.bundle_view_patches),
modifier = Modifier.clickable(enabled = patchesClickable, onClick = onPatchesClick) modifier = Modifier.clickable(
enabled = patchesClickable,
onClick = onPatchesClick
)
) { ) {
if (patchesClickable) if (patchesClickable) {
Icon( Icon(
Icons.AutoMirrored.Outlined.ArrowRight, Icons.AutoMirrored.Outlined.ArrowRight,
stringResource(R.string.patches) stringResource(R.string.patches)
) )
}
} }
version?.let { extraFields()
BundleListItem( }
headlineText = stringResource(R.string.version), }
supportingText = it,
) @Composable
} private fun Tag(
icon: ImageVector,
text: String
) {
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.outline,
)
Text(
text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
)
} }
} }

View File

@ -7,17 +7,9 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.ArrowRight import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.DeleteOutline
import androidx.compose.material.icons.outlined.Update import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.*
import androidx.compose.material3.Icon import androidx.compose.runtime.*
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
@ -72,7 +64,7 @@ fun BundleInformationDialog(
Scaffold( Scaffold(
topBar = { topBar = {
BundleTopBar( BundleTopBar(
title = bundleName, title = stringResource(R.string.patch_bundle_field),
onBackClick = onDismissRequest, onBackClick = onDismissRequest,
backIcon = { backIcon = {
Icon( Icon(
@ -105,10 +97,9 @@ fun BundleInformationDialog(
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
isDefault = bundle.isDefault, isDefault = bundle.isDefault,
name = bundleName, name = bundleName,
onNameChange = { composableScope.launch { bundle.setName(it) } },
remoteUrl = bundle.asRemoteOrNull?.endpoint, remoteUrl = bundle.asRemoteOrNull?.endpoint,
patchCount = patchCount, patchCount = patchCount,
version = props?.versionInfo?.patches, version = props?.version,
autoUpdate = props?.autoUpdate ?: false, autoUpdate = props?.autoUpdate ?: false,
onAutoUpdateChange = { onAutoUpdateChange = {
composableScope.launch { composableScope.launch {

View File

@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.icons.outlined.Warning import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -27,6 +26,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -45,7 +45,7 @@ fun BundleItem(
val state by bundle.state.collectAsStateWithLifecycle() val state by bundle.state.collectAsStateWithLifecycle()
val version by remember(bundle) { val version by remember(bundle) {
bundle.propsFlow().map { props -> props?.versionInfo?.patches } bundle.propsFlow().map { props -> props?.version }
}.collectAsStateWithLifecycle(null) }.collectAsStateWithLifecycle(null)
val name by bundle.nameState val name by bundle.nameState
@ -71,7 +71,7 @@ fun BundleItem(
), ),
leadingContent = if (selectable) { leadingContent = if (selectable) {
{ {
Checkbox( HapticCheckbox(
checked = isBundleSelected, checked = isBundleSelected,
onCheckedChange = toggleSelection, onCheckedChange = toggleSelection,
) )

View File

@ -1,33 +1,35 @@
package app.revanced.manager.ui.component.bundle package app.revanced.manager.ui.component.bundle
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.outlined.Lightbulb import androidx.compose.material3.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.component.ArrowButton
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.NotificationCard
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -35,7 +37,8 @@ fun BundlePatchesDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
bundle: PatchBundleSource, bundle: PatchBundleSource,
) { ) {
var informationCardVisible by remember { mutableStateOf(true) } var showAllVersions by rememberSaveable { mutableStateOf(false) }
var showOptions by rememberSaveable { mutableStateOf(false) }
val state by bundle.state.collectAsStateWithLifecycle() val state by bundle.state.collectAsStateWithLifecycle()
Dialog( Dialog(
@ -62,44 +65,212 @@ fun BundlePatchesDialog(
LazyColumnWithScrollbar( LazyColumnWithScrollbar(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(paddingValues) .padding(paddingValues),
.padding(16.dp) verticalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(16.dp)
) { ) {
item {
AnimatedVisibility(visible = informationCardVisible) {
NotificationCard(
icon = Icons.Outlined.Lightbulb,
text = stringResource(R.string.tap_on_patches),
onDismiss = { informationCardVisible = false }
)
}
}
state.patchBundleOrNull()?.let { bundle -> state.patchBundleOrNull()?.let { bundle ->
items(bundle.patches.size) { bundleIndex -> items(bundle.patches) { patch ->
val patch = bundle.patches[bundleIndex] PatchItem(
ListItem( patch,
headlineContent = { showAllVersions,
Text( onExpandVersions = { showAllVersions = !showAllVersions },
text = patch.name, showOptions,
style = MaterialTheme.typography.bodyLarge, onExpandOptions = { showOptions = !showOptions }
color = MaterialTheme.colorScheme.onSurface
)
},
supportingContent = {
patch.description?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
) )
HorizontalDivider()
} }
} }
} }
} }
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun PatchItem(
patch: PatchInfo,
expandVersions: Boolean,
onExpandVersions: () -> Unit,
expandOptions: Boolean,
onExpandOptions: () -> Unit
) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.then(
if (patch.options.isNullOrEmpty()) Modifier else Modifier
.clip(RoundedCornerShape(8.dp))
.clickable(onClick = onExpandOptions),
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Absolute.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = patch.name,
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
if (!patch.options.isNullOrEmpty()) {
ArrowButton(expanded = expandOptions, onClick = null)
}
}
patch.description?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium
)
}
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
if (patch.compatiblePackages.isNullOrEmpty()) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
PatchInfoChip(
text = "$PACKAGE_ICON ${stringResource(R.string.bundle_view_patches_any_package)}"
)
PatchInfoChip(
text = "$VERSION_ICON ${stringResource(R.string.bundle_view_patches_any_version)}"
)
}
} else {
patch.compatiblePackages.forEach { compatiblePackage ->
val packageName = compatiblePackage.packageName
val versions = compatiblePackage.versions.orEmpty().reversed()
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
PatchInfoChip(
modifier = Modifier.align(Alignment.CenterVertically),
text = "$PACKAGE_ICON $packageName"
)
if (versions.isNotEmpty()) {
if (expandVersions) {
versions.forEach { version ->
PatchInfoChip(
modifier = Modifier.align(Alignment.CenterVertically),
text = "$VERSION_ICON $version"
)
}
} else {
PatchInfoChip(
modifier = Modifier.align(Alignment.CenterVertically),
text = "$VERSION_ICON ${versions.first()}"
)
}
if (versions.size > 1) {
PatchInfoChip(
onClick = onExpandVersions,
text = if (expandVersions) stringResource(R.string.less) else "+${versions.size - 1}"
)
}
}
}
}
}
}
if (!patch.options.isNullOrEmpty()) {
AnimatedVisibility(visible = expandOptions) {
val options = patch.options
Column {
options.forEachIndexed { i, option ->
OutlinedCard(
modifier = Modifier.fillMaxWidth(),
colors = CardColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onSurface,
disabledContainerColor = Color.Transparent,
disabledContentColor = MaterialTheme.colorScheme.onSurface
), shape = when {
options.size == 1 -> RoundedCornerShape(8.dp)
i == 0 -> RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)
i == options.lastIndex -> RoundedCornerShape(
bottomStart = 8.dp,
bottomEnd = 8.dp
)
else -> RoundedCornerShape(0.dp)
}
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = option.title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Text(
text = option.description,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}
}
}
}
}
@Composable
fun PatchInfoChip(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
text: String
) {
val shape = RoundedCornerShape(8.0.dp)
val cardModifier = if (onClick != null) {
Modifier
.clip(shape)
.clickable(onClick = onClick)
} else {
Modifier
}
OutlinedCard(
modifier = modifier.then(cardModifier),
colors = CardColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onSurface,
disabledContainerColor = Color.Transparent,
disabledContentColor = MaterialTheme.colorScheme.onSurface
),
shape = shape,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.20f))
) {
Row(
modifier = Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text(
text,
overflow = TextOverflow.Ellipsis,
softWrap = false,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
const val PACKAGE_ICON = "\uD83D\uDCE6"
const val VERSION_ICON = "\uD83C\uDFAF"

View File

@ -10,26 +10,9 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Topic import androidx.compose.material.icons.filled.Topic
import androidx.compose.material3.Checkbox import androidx.compose.material3.*
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.*
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
@ -37,20 +20,21 @@ import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.AlertDialogExtended import app.revanced.manager.ui.component.AlertDialogExtended
import app.revanced.manager.ui.component.TextHorizontalPadding import app.revanced.manager.ui.component.TextHorizontalPadding
import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.model.BundleType import app.revanced.manager.ui.model.BundleType
import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.BIN_MIMETYPE
import app.revanced.manager.util.JAR_MIMETYPE import app.revanced.manager.util.transparentListItemColors
@Composable @Composable
fun ImportPatchBundleDialog( fun ImportPatchBundleDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onRemoteSubmit: (String, Boolean) -> Unit, onRemoteSubmit: (String, Boolean) -> Unit,
onLocalSubmit: (Uri, Uri?) -> Unit onLocalSubmit: (Uri) -> Unit
) { ) {
var currentStep by rememberSaveable { mutableIntStateOf(0) } var currentStep by rememberSaveable { mutableIntStateOf(0) }
var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) } var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) }
var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) } var patchBundle by rememberSaveable { mutableStateOf<Uri?>(null) }
var integrations by rememberSaveable { mutableStateOf<Uri?>(null) }
var remoteUrl by rememberSaveable { mutableStateOf("") } var remoteUrl by rememberSaveable { mutableStateOf("") }
var autoUpdate by rememberSaveable { mutableStateOf(false) } var autoUpdate by rememberSaveable { mutableStateOf(false) }
@ -60,16 +44,7 @@ fun ImportPatchBundleDialog(
} }
fun launchPatchActivity() { fun launchPatchActivity() {
patchActivityLauncher.launch(JAR_MIMETYPE) patchActivityLauncher.launch(BIN_MIMETYPE)
}
val integrationsActivityLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { integrations = it }
}
fun launchIntegrationsActivity() {
integrationsActivityLauncher.launch(APK_MIMETYPE)
} }
val steps = listOf<@Composable () -> Unit>( val steps = listOf<@Composable () -> Unit>(
@ -82,11 +57,9 @@ fun ImportPatchBundleDialog(
ImportBundleStep( ImportBundleStep(
bundleType, bundleType,
patchBundle, patchBundle,
integrations,
remoteUrl, remoteUrl,
autoUpdate, autoUpdate,
{ launchPatchActivity() }, { launchPatchActivity() },
{ launchIntegrationsActivity() },
{ remoteUrl = it }, { remoteUrl = it },
{ autoUpdate = it } { autoUpdate = it }
) )
@ -114,13 +87,7 @@ fun ImportPatchBundleDialog(
enabled = inputsAreValid, enabled = inputsAreValid,
onClick = { onClick = {
when (bundleType) { when (bundleType) {
BundleType.Local -> patchBundle?.let { BundleType.Local -> patchBundle?.let(onLocalSubmit)
onLocalSubmit(
it,
integrations
)
}
BundleType.Remote -> onRemoteSubmit(remoteUrl, autoUpdate) BundleType.Remote -> onRemoteSubmit(remoteUrl, autoUpdate)
} }
} }
@ -170,11 +137,12 @@ fun SelectBundleTypeStep(
overlineContent = { Text(stringResource(R.string.recommended)) }, overlineContent = { Text(stringResource(R.string.recommended)) },
supportingContent = { Text(stringResource(R.string.remote_bundle_description)) }, supportingContent = { Text(stringResource(R.string.remote_bundle_description)) },
leadingContent = { leadingContent = {
RadioButton( HapticRadioButton(
selected = bundleType == BundleType.Remote, selected = bundleType == BundleType.Remote,
onClick = null onClick = null
) )
} },
colors = transparentListItemColors
) )
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
ListItem( ListItem(
@ -186,11 +154,12 @@ fun SelectBundleTypeStep(
supportingContent = { Text(stringResource(R.string.local_bundle_description)) }, supportingContent = { Text(stringResource(R.string.local_bundle_description)) },
overlineContent = { }, overlineContent = { },
leadingContent = { leadingContent = {
RadioButton( HapticRadioButton(
selected = bundleType == BundleType.Local, selected = bundleType == BundleType.Local,
onClick = null onClick = null
) )
} },
colors = transparentListItemColors
) )
} }
} }
@ -201,11 +170,9 @@ fun SelectBundleTypeStep(
fun ImportBundleStep( fun ImportBundleStep(
bundleType: BundleType, bundleType: BundleType,
patchBundle: Uri?, patchBundle: Uri?,
integrations: Uri?,
remoteUrl: String, remoteUrl: String,
autoUpdate: Boolean, autoUpdate: Boolean,
launchPatchActivity: () -> Unit, launchPatchActivity: () -> Unit,
launchIntegrationsActivity: () -> Unit,
onRemoteUrlChange: (String) -> Unit, onRemoteUrlChange: (String) -> Unit,
onAutoUpdateChange: (Boolean) -> Unit onAutoUpdateChange: (Boolean) -> Unit
) { ) {
@ -225,19 +192,8 @@ fun ImportBundleStep(
Icon(imageVector = Icons.Default.Topic, contentDescription = null) Icon(imageVector = Icons.Default.Topic, contentDescription = null)
} }
}, },
modifier = Modifier.clickable { launchPatchActivity() } modifier = Modifier.clickable { launchPatchActivity() },
) colors = transparentListItemColors
ListItem(
headlineContent = {
Text(stringResource(R.string.integrations_field))
},
supportingContent = { Text(stringResource(if (integrations != null) R.string.file_field_set else R.string.file_field_not_set)) },
trailingContent = {
IconButton(onClick = launchIntegrationsActivity) {
Icon(imageVector = Icons.Default.Topic, contentDescription = null)
}
},
modifier = Modifier.clickable { launchIntegrationsActivity() }
) )
} }
} }
@ -263,7 +219,7 @@ fun ImportBundleStep(
headlineContent = { Text(stringResource(R.string.auto_update)) }, headlineContent = { Text(stringResource(R.string.auto_update)) },
leadingContent = { leadingContent = {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
Checkbox( HapticCheckbox(
checked = autoUpdate, checked = autoUpdate,
onCheckedChange = { onCheckedChange = {
onAutoUpdateChange(!autoUpdate) onAutoUpdateChange(!autoUpdate)
@ -271,6 +227,7 @@ fun ImportBundleStep(
) )
} }
}, },
colors = transparentListItemColors
) )
} }
} }

View File

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

View File

@ -0,0 +1,41 @@
package app.revanced.manager.ui.component.haptics
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.FloatingActionButtonElevation
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import app.revanced.manager.util.withHapticFeedback
@Composable
fun HapticExtendedFloatingActionButton (
text: @Composable () -> Unit,
icon: @Composable () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
expanded: Boolean = true,
shape: Shape = FloatingActionButtonDefaults.extendedFabShape,
containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
ExtendedFloatingActionButton(
text = text,
icon = icon,
onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY),
modifier = modifier,
expanded = expanded,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
elevation = elevation,
interactionSource = interactionSource
)
}

View File

@ -0,0 +1,37 @@
package app.revanced.manager.ui.component.haptics
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.FloatingActionButtonElevation
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import app.revanced.manager.util.withHapticFeedback
@Composable
fun HapticFloatingActionButton (
onClick: () -> Unit,
modifier: Modifier = Modifier,
shape: Shape = FloatingActionButtonDefaults.shape,
containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable () -> Unit,
) {
FloatingActionButton(
onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY),
modifier = modifier,
shape = shape,
containerColor = containerColor,
contentColor = contentColor,
elevation = elevation,
interactionSource = interactionSource,
content = content
)
}

View File

@ -0,0 +1,38 @@
package app.revanced.manager.ui.component.haptics
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.RadioButton
import androidx.compose.material3.RadioButtonColors
import androidx.compose.material3.RadioButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
@Composable
fun HapticRadioButton(
selected: Boolean,
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
colors: RadioButtonColors = RadioButtonDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
val view = LocalView.current
RadioButton(
selected = selected,
onClick = onClick?.let {
{
// Perform haptic feedback
if (!selected) view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
it()
}
},
modifier = modifier,
enabled = enabled,
colors = colors,
interactionSource = interactionSource
)
}

View File

@ -0,0 +1,41 @@
package app.revanced.manager.ui.component.haptics
import android.os.Build
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchColors
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@Composable
fun HapticSwitch(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
thumbContent: (@Composable () -> Unit)? = null,
enabled: Boolean = true,
colors: SwitchColors = SwitchDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
Switch(
checked = checked,
onCheckedChange = { newChecked ->
val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
when {
newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON
newChecked -> HapticFeedbackConstants.VIRTUAL_KEY
!newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF
!newChecked -> HapticFeedbackConstants.CLOCK_TICK
}
onCheckedChange(newChecked)
},
modifier = modifier,
thumbContent = thumbContent,
enabled = enabled,
colors = colors,
interactionSource = interactionSource,
)
}

View File

@ -0,0 +1,36 @@
package app.revanced.manager.ui.component.haptics
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import app.revanced.manager.util.withHapticFeedback
@Composable
fun HapticTab (
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
text: @Composable (() -> Unit)? = null,
icon: @Composable (() -> Unit)? = null,
selectedContentColor: Color = LocalContentColor.current,
unselectedContentColor: Color = selectedContentColor,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
Tab(
selected = selected,
onClick = onClick.withHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY),
modifier = modifier,
enabled = enabled,
text = text,
icon = icon,
selectedContentColor = selectedContentColor,
unselectedContentColor = unselectedContentColor,
interactionSource = interactionSource
)
}

View File

@ -2,11 +2,7 @@ package app.revanced.manager.ui.component.patcher
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.AlertDialog import androidx.compose.material3.*
import androidx.compose.material3.Button
import androidx.compose.material3.ListItem
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -16,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.util.transparentListItemColors import app.revanced.manager.util.transparentListItemColors
@Composable @Composable
@ -28,7 +25,7 @@ fun InstallPickerDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
dismissButton = { dismissButton = {
Button(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel)) Text(stringResource(R.string.cancel))
} }
}, },
@ -49,7 +46,7 @@ fun InstallPickerDialog(
ListItem( ListItem(
modifier = Modifier.clickable { selectedInstallType = it }, modifier = Modifier.clickable { selectedInstallType = it },
leadingContent = { leadingContent = {
RadioButton( HapticRadioButton(
selected = selectedInstallType == it, selected = selectedInstallType == it,
onClick = null onClick = null
) )

View File

@ -20,53 +20,31 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.*
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.*
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog as ComposeDialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.patcher.patch.Option import app.revanced.manager.patcher.patch.Option
import app.revanced.manager.ui.component.AlertDialogExtended import app.revanced.manager.ui.component.*
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.FloatInputDialog import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.component.IntInputDialog import app.revanced.manager.ui.component.haptics.HapticSwitch
import app.revanced.manager.ui.component.LongInputDialog
import app.revanced.manager.util.isScrollingUp import app.revanced.manager.util.isScrollingUp
import app.revanced.manager.util.mutableStateSetOf import app.revanced.manager.util.mutableStateSetOf
import app.revanced.manager.util.saver.snapshotStateListSaver import app.revanced.manager.util.saver.snapshotStateListSaver
@ -81,6 +59,8 @@ import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyColumnState import sh.calvin.reorderable.rememberReorderableLazyColumnState
import java.io.Serializable import java.io.Serializable
import kotlin.random.Random import kotlin.random.Random
import kotlin.reflect.typeOf
import androidx.compose.ui.window.Dialog as ComposeDialog
private class OptionEditorScope<T : Any>( private class OptionEditorScope<T : Any>(
private val editor: OptionEditor<T>, private val editor: OptionEditor<T>,
@ -118,17 +98,17 @@ private interface OptionEditor<T : Any> {
fun Dialog(scope: OptionEditorScope<T>) fun Dialog(scope: OptionEditorScope<T>)
} }
private inline fun <reified T : Serializable> OptionEditor<T>.toMapEditorElements() = arrayOf(
typeOf<T>() to this,
typeOf<List<T>>() to ListOptionEditor(this)
)
private val optionEditors = mapOf( private val optionEditors = mapOf(
"Boolean" to BooleanOptionEditor, *BooleanOptionEditor.toMapEditorElements(),
"String" to StringOptionEditor, *StringOptionEditor.toMapEditorElements(),
"Int" to IntOptionEditor, *IntOptionEditor.toMapEditorElements(),
"Long" to LongOptionEditor, *LongOptionEditor.toMapEditorElements(),
"Float" to FloatOptionEditor, *FloatOptionEditor.toMapEditorElements()
"BooleanArray" to ListOptionEditor(BooleanOptionEditor),
"StringArray" to ListOptionEditor(StringOptionEditor),
"IntArray" to ListOptionEditor(IntOptionEditor),
"LongArray" to ListOptionEditor(LongOptionEditor),
"FloatArray" to ListOptionEditor(FloatOptionEditor),
) )
@Composable @Composable
@ -167,7 +147,7 @@ fun <T : Any> OptionItem(option: Option<T>, value: T?, setValue: (T?) -> Unit) {
val baseOptionEditor = val baseOptionEditor =
optionEditors.getOrDefault(option.type, UnknownTypeEditor) as OptionEditor<T> optionEditors.getOrDefault(option.type, UnknownTypeEditor) as OptionEditor<T>
if (option.type != "Boolean" && option.presets != null) PresetOptionEditor(baseOptionEditor) if (option.type != typeOf<Boolean>() && option.presets != null) PresetOptionEditor(baseOptionEditor)
else baseOptionEditor else baseOptionEditor
} }
@ -336,7 +316,7 @@ private object BooleanOptionEditor : OptionEditor<Boolean> {
@Composable @Composable
override fun ListItemTrailingContent(scope: OptionEditorScope<Boolean>) { override fun ListItemTrailingContent(scope: OptionEditorScope<Boolean>) {
Switch(checked = scope.current, onCheckedChange = scope.setValue) HapticSwitch(checked = scope.current, onCheckedChange = scope.setValue)
} }
@Composable @Composable
@ -423,7 +403,7 @@ private class PresetOptionEditor<T : Any>(private val innerEditor: OptionEditor<
headlineContent = { Text(title) }, headlineContent = { Text(title) },
supportingContent = value?.toString()?.let { { Text(it) } }, supportingContent = value?.toString()?.let { { Text(it) } },
leadingContent = { leadingContent = {
RadioButton( HapticRadioButton(
selected = selectedPreset == presetKey, selected = selectedPreset == presetKey,
onClick = { selectedPreset = presetKey } onClick = { selectedPreset = presetKey }
) )
@ -453,7 +433,7 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
option.key, option.key,
option.description, option.description,
option.required, option.required,
option.type.removeSuffix("Array"), option.type.arguments.first().type!!,
null, null,
null null
) { true } ) { true }
@ -570,7 +550,7 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
floatingActionButton = { floatingActionButton = {
if (deleteMode) return@Scaffold if (deleteMode) return@Scaffold
ExtendedFloatingActionButton( HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.add)) }, text = { Text(stringResource(R.string.add)) },
icon = { icon = {
Icon( Icon(

View File

@ -2,13 +2,13 @@ package app.revanced.manager.ui.component.settings
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import app.revanced.manager.domain.manager.base.Preference import app.revanced.manager.domain.manager.base.Preference
import app.revanced.manager.ui.component.haptics.HapticSwitch
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -45,7 +45,7 @@ fun BooleanItem(
headlineContent = stringResource(headline), headlineContent = stringResource(headline),
supportingContent = stringResource(description), supportingContent = stringResource(description),
trailingContent = { trailingContent = {
Switch( HapticSwitch(
checked = value, checked = value,
onCheckedChange = onValueChange, onCheckedChange = onValueChange,
) )

View File

@ -116,7 +116,6 @@ fun AppSelectorScreen(
}, },
colors = transparentListItemColors colors = transparentListItemColors
) )
} }
} }
} else { } else {

View File

@ -33,6 +33,8 @@ import app.revanced.manager.ui.component.AvailableUpdateDialog
import app.revanced.manager.ui.component.NotificationCard import app.revanced.manager.ui.component.NotificationCard
import app.revanced.manager.ui.component.bundle.BundleItem import app.revanced.manager.ui.component.bundle.BundleItem
import app.revanced.manager.ui.component.bundle.BundleTopBar import app.revanced.manager.ui.component.bundle.BundleTopBar
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
import app.revanced.manager.ui.component.haptics.HapticTab
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
import app.revanced.manager.ui.viewmodel.DashboardViewModel import app.revanced.manager.ui.viewmodel.DashboardViewModel
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
@ -81,9 +83,9 @@ fun DashboardScreen(
if (showAddBundleDialog) { if (showAddBundleDialog) {
ImportPatchBundleDialog( ImportPatchBundleDialog(
onDismiss = { showAddBundleDialog = false }, onDismiss = { showAddBundleDialog = false },
onLocalSubmit = { patches, integrations -> onLocalSubmit = { patches ->
showAddBundleDialog = false showAddBundleDialog = false
vm.createLocalSource(patches, integrations) vm.createLocalSource(patches)
}, },
onRemoteSubmit = { url, autoUpdate -> onRemoteSubmit = { url, autoUpdate ->
showAddBundleDialog = false showAddBundleDialog = false
@ -172,7 +174,7 @@ fun DashboardScreen(
} }
}, },
floatingActionButton = { floatingActionButton = {
FloatingActionButton( HapticFloatingActionButton(
onClick = { onClick = {
vm.cancelSourceSelection() vm.cancelSourceSelection()
@ -185,7 +187,7 @@ fun DashboardScreen(
DashboardPage.BUNDLES.ordinal DashboardPage.BUNDLES.ordinal
) )
} }
return@FloatingActionButton return@HapticFloatingActionButton
} }
onAppSelectorClick() onAppSelectorClick()
@ -205,7 +207,7 @@ fun DashboardScreen(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) { ) {
DashboardPage.entries.forEachIndexed { index, page -> DashboardPage.entries.forEachIndexed { index, page ->
Tab( HapticTab(
selected = pagerState.currentPage == index, selected = pagerState.currentPage == index,
onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } }, onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } },
text = { Text(stringResource(page.titleResId)) }, text = { Text(stringResource(page.titleResId)) },

View File

@ -81,7 +81,7 @@ fun InstalledAppInfoScreen(
AppInfo(viewModel.appInfo) { AppInfo(viewModel.appInfo) {
Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium) Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
if (viewModel.installedApp.installType == InstallType.ROOT) { if (viewModel.installedApp.installType == InstallType.MOUNT) {
Text( Text(
text = if (viewModel.isMounted) { text = if (viewModel.isMounted) {
stringResource(R.string.mounted) stringResource(R.string.mounted)
@ -112,7 +112,7 @@ fun InstalledAppInfoScreen(
onClick = viewModel::uninstall onClick = viewModel::uninstall
) )
InstallType.ROOT -> { InstallType.MOUNT -> {
SegmentedButton( SegmentedButton(
icon = Icons.Outlined.SettingsBackupRestore, icon = Icons.Outlined.SettingsBackupRestore,
text = stringResource(R.string.unpatch), text = stringResource(R.string.unpatch),
@ -138,7 +138,7 @@ fun InstalledAppInfoScreen(
onPatchClick(viewModel.installedApp.originalPackageName, it) onPatchClick(viewModel.installedApp.originalPackageName, it)
} }
}, },
enabled = viewModel.installedApp.installType != InstallType.ROOT || viewModel.rootInstaller.hasRootAccess() enabled = viewModel.installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess()
) )
} }

View File

@ -5,12 +5,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -18,17 +13,8 @@ import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material.icons.outlined.FileDownload import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.PostAdd import androidx.compose.material.icons.outlined.PostAdd
import androidx.compose.material.icons.outlined.Save import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.AlertDialog import androidx.compose.material3.*
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
@ -42,8 +28,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.ui.component.AppScaffold import app.revanced.manager.ui.component.AppScaffold
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.InstallerStatusDialog
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.patcher.InstallPickerDialog import app.revanced.manager.ui.component.patcher.InstallPickerDialog
import app.revanced.manager.ui.component.patcher.Steps import app.revanced.manager.ui.component.patcher.Steps
import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.State
@ -96,6 +85,9 @@ fun PatcherScreen(
onConfirm = vm::install onConfirm = vm::install
) )
if (vm.installerStatusDialogModel.packageInstallerStatus != null)
InstallerStatusDialog(vm.installerStatusDialogModel)
val activityLauncher = rememberLauncherForActivityResult( val activityLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(), contract = ActivityResultContracts.StartActivityForResult(),
onResult = vm::handleActivityResult onResult = vm::handleActivityResult
@ -139,7 +131,7 @@ fun PatcherScreen(
actions = { actions = {
IconButton( IconButton(
onClick = { exportApkLauncher.launch("${vm.packageName}.apk") }, onClick = { exportApkLauncher.launch("${vm.packageName}.apk") },
enabled = canInstall enabled = patcherSucceeded == true
) { ) {
Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk)) Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk))
} }
@ -152,7 +144,7 @@ fun PatcherScreen(
}, },
floatingActionButton = { floatingActionButton = {
AnimatedVisibility(visible = canInstall) { AnimatedVisibility(visible = canInstall) {
ExtendedFloatingActionButton( HapticExtendedFloatingActionButton(
text = { text = {
Text( Text(
stringResource(if (vm.installedPackageName == null) R.string.install_app else R.string.open_app) stringResource(if (vm.installedPackageName == null) R.string.install_app else R.string.open_app)
@ -171,7 +163,8 @@ fun PatcherScreen(
}, },
onClick = { onClick = {
if (vm.installedPackageName == null) if (vm.installedPackageName == null)
showInstallPicker = true if (vm.isDeviceRooted()) showInstallPicker = true
else vm.install(InstallType.DEFAULT)
else vm.open() else vm.open()
} }
) )
@ -208,4 +201,4 @@ fun PatcherScreen(
} }
} }
} }
} }

View File

@ -35,6 +35,9 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.SafeguardDialog import app.revanced.manager.ui.component.SafeguardDialog
import app.revanced.manager.ui.component.SearchView import app.revanced.manager.ui.component.SearchView
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.patches.OptionItem import app.revanced.manager.ui.component.patches.OptionItem
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED
@ -299,7 +302,7 @@ fun PatchesSelectorScreen(
floatingActionButton = { floatingActionButton = {
if (!showPatchButton) return@Scaffold if (!showPatchButton) return@Scaffold
ExtendedFloatingActionButton( HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.save)) }, text = { Text(stringResource(R.string.save)) },
icon = { icon = {
Icon( Icon(
@ -327,7 +330,7 @@ fun PatchesSelectorScreen(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
) { ) {
bundles.forEachIndexed { index, bundle -> bundles.forEachIndexed { index, bundle ->
Tab( HapticTab(
selected = pagerState.currentPage == index, selected = pagerState.currentPage == index,
onClick = { onClick = {
composableScope.launch { composableScope.launch {
@ -444,7 +447,7 @@ private fun PatchItem(
.clickable(onClick = onToggle) .clickable(onClick = onToggle)
.fillMaxSize(), .fillMaxSize(),
leadingContent = { leadingContent = {
Checkbox( HapticCheckbox(
checked = selected, checked = selected,
onCheckedChange = { onToggle() }, onCheckedChange = { onToggle() },
enabled = supported enabled = supported

View File

@ -12,14 +12,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.filled.AutoFixHigh import androidx.compose.material.icons.filled.AutoFixHigh
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.*
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -38,6 +31,7 @@ import app.revanced.manager.ui.component.AppInfo
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.destination.SelectedAppInfoDestination import app.revanced.manager.ui.destination.SelectedAppInfoDestination
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
@ -49,11 +43,7 @@ import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.enabled import app.revanced.manager.util.enabled
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.transparentListItemColors import app.revanced.manager.util.transparentListItemColors
import dev.olshevski.navigation.reimagined.AnimatedNavHost import dev.olshevski.navigation.reimagined.*
import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.navigate
import dev.olshevski.navigation.reimagined.pop
import dev.olshevski.navigation.reimagined.rememberNavController
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
@ -110,7 +100,7 @@ fun SelectedAppInfoScreen(
floatingActionButton = { floatingActionButton = {
if (error != null) return@Scaffold if (error != null) return@Scaffold
ExtendedFloatingActionButton( HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.patch)) }, text = { Text(stringResource(R.string.patch)) },
icon = { icon = {
Icon( Icon(
@ -299,7 +289,7 @@ private fun AppSourceSelectorDialog(
item(key = "installed") { item(key = "installed") {
val (usable, text) = when { val (usable, text) = when {
// Mounted apps must be unpatched before patching, which cannot be done without root access. // Mounted apps must be unpatched before patching, which cannot be done without root access.
meta?.installType == InstallType.ROOT && !hasRoot -> false to "Mounted apps cannot be patched again without root access" meta?.installType == InstallType.MOUNT && !hasRoot -> false to "Mounted apps cannot be patched again without root access"
// Patching already patched apps is not allowed because patches expect unpatched apps. // Patching already patched apps is not allowed because patches expect unpatched apps.
meta?.installType == InstallType.DEFAULT -> false to stringResource(R.string.already_patched) meta?.installType == InstallType.DEFAULT -> false to stringResource(R.string.already_patched)
else -> true to app.version else -> true to app.version

View File

@ -66,7 +66,7 @@ fun SettingsScreen(
) to SettingsDestination.Advanced, ) to SettingsDestination.Advanced,
Triple( Triple(
R.string.about, R.string.about,
R.string.about_description, R.string.app_name,
Icons.Outlined.Info Icons.Outlined.Info
) to SettingsDestination.About, ) to SettingsDestination.About,
) )

View File

@ -1,10 +1,15 @@
package app.revanced.manager.ui.screen.settings package app.revanced.manager.ui.screen.settings
import android.app.ActivityManager import android.app.ActivityManager
import android.content.ClipData
import android.content.ClipboardManager
import android.os.Build import android.os.Build
import android.view.HapticFeedbackConstants
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Api import androidx.compose.material.icons.outlined.Api
@ -28,9 +33,11 @@ import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.IntegerItem import app.revanced.manager.ui.component.settings.IntegerItem
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
import app.revanced.manager.util.toast
import app.revanced.manager.util.withHapticFeedback
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
fun AdvancedSettingsScreen( fun AdvancedSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
@ -82,15 +89,6 @@ fun AdvancedSettingsScreen(
} }
) )
val exportDebugLogsLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) {
it?.let(vm::exportDebugLogs)
}
SettingsListItem(
headlineContent = stringResource(R.string.debug_logs_export),
modifier = Modifier.clickable { exportDebugLogsLauncher.launch(vm.debugLogFileName) }
)
GroupHeader(stringResource(R.string.patcher)) GroupHeader(stringResource(R.string.patcher))
BooleanItem( BooleanItem(
preference = vm.prefs.useProcessRuntime, preference = vm.prefs.useProcessRuntime,
@ -104,12 +102,6 @@ fun AdvancedSettingsScreen(
headline = R.string.process_runtime_memory_limit, headline = R.string.process_runtime_memory_limit,
description = R.string.process_runtime_memory_limit_description, description = R.string.process_runtime_memory_limit_description,
) )
BooleanItem(
preference = vm.prefs.multithreadingDexFileWriter,
coroutineScope = vm.viewModelScope,
headline = R.string.multithreaded_dex_file_writer,
description = R.string.multithreaded_dex_file_writer_description,
)
GroupHeader(stringResource(R.string.safeguards)) GroupHeader(stringResource(R.string.safeguards))
BooleanItem( BooleanItem(
@ -138,16 +130,37 @@ fun AdvancedSettingsScreen(
) )
GroupHeader(stringResource(R.string.debugging)) GroupHeader(stringResource(R.string.debugging))
val exportDebugLogsLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) {
it?.let(vm::exportDebugLogs)
}
SettingsListItem( SettingsListItem(
headlineContent = stringResource(R.string.about_device), headlineContent = stringResource(R.string.debug_logs_export),
supportingContent = """ modifier = Modifier.clickable { exportDebugLogsLauncher.launch(vm.debugLogFileName) }
**Version**: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) )
**Build type**: ${BuildConfig.BUILD_TYPE} val clipboard = remember { context.getSystemService<ClipboardManager>()!! }
**Model**: ${Build.MODEL} val deviceContent = """
**Android version**: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT}) Version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})
**Supported Archs**: ${Build.SUPPORTED_ABIS.joinToString(", ")} Build type: ${BuildConfig.BUILD_TYPE}
**Memory limit**: $memoryLimit Model: ${Build.MODEL}
Android version: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT})
Supported Archs: ${Build.SUPPORTED_ABIS.joinToString(", ")}
Memory limit: $memoryLimit
""".trimIndent() """.trimIndent()
SettingsListItem(
modifier = Modifier.combinedClickable(
onClick = { },
onLongClickLabel = stringResource(R.string.copy_to_clipboard),
onLongClick = {
clipboard.setPrimaryClip(
ClipData.newPlainText("Device Information", deviceContent)
)
context.toast(context.getString(R.string.toast_copied_to_clipboard))
}.withHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
),
headlineContent = stringResource(R.string.about_device),
supportingContent = deviceContent
) )
} }
} }

View File

@ -2,7 +2,20 @@ package app.revanced.manager.ui.screen.settings
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState

View File

@ -32,7 +32,7 @@ fun DeveloperOptionsScreen(
Column(modifier = Modifier.padding(paddingValues)) { Column(modifier = Modifier.padding(paddingValues)) {
GroupHeader(stringResource(R.string.patch_bundles_section)) GroupHeader(stringResource(R.string.patch_bundles_section))
SettingsListItem( SettingsListItem(
headlineContent = stringResource(R.string.patch_bundles_redownload), headlineContent = stringResource(R.string.patch_bundles_force_download),
modifier = Modifier.clickable(onClick = vm::redownloadBundles) modifier = Modifier.clickable(onClick = vm::redownloadBundles)
) )
SettingsListItem( SettingsListItem(

View File

@ -10,7 +10,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -41,9 +40,9 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ExceptionViewerDialog import app.revanced.manager.ui.component.ExceptionViewerDialog
import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.haptics.HapticCheckbox
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.DownloadsViewModel import app.revanced.manager.ui.viewmodel.DownloadsViewModel
import app.revanced.manager.util.PM
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import java.security.MessageDigest import java.security.MessageDigest
@ -178,7 +177,7 @@ fun DownloadsSettingsScreen(
is DownloaderPluginState.Untrusted -> R.string.downloader_plugin_state_untrusted is DownloaderPluginState.Untrusted -> R.string.downloader_plugin_state_untrusted
} }
), ),
trailingContent = { Text(packageInfo.versionName) } trailingContent = { Text(packageInfo.versionName!!) }
) )
} }
} }
@ -202,7 +201,7 @@ fun DownloadsSettingsScreen(
modifier = Modifier.clickable { viewModel.toggleApp(app) }, modifier = Modifier.clickable { viewModel.toggleApp(app) },
headlineContent = app.packageName, headlineContent = app.packageName,
leadingContent = (@Composable { leadingContent = (@Composable {
Checkbox( HapticCheckbox(
checked = selected, checked = selected,
onCheckedChange = { viewModel.toggleApp(app) } onCheckedChange = { viewModel.toggleApp(app) }
) )

View File

@ -2,8 +2,18 @@ package app.revanced.manager.ui.screen.settings
import android.os.Build import android.os.Build
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Column
import androidx.compose.material3.* import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -18,6 +28,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.haptics.HapticRadioButton
import app.revanced.manager.ui.component.settings.BooleanItem import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
@ -96,14 +107,14 @@ private fun ThemePicker(
title = { Text(stringResource(R.string.theme)) }, title = { Text(stringResource(R.string.theme)) },
text = { text = {
Column { Column {
Theme.values().forEach { Theme.entries.forEach {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { selectedTheme = it }, .clickable { selectedTheme = it },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
RadioButton( HapticRadioButton(
selected = selectedTheme == it, selected = selectedTheme == it,
onClick = { selectedTheme = it }) onClick = { selectedTheme = it })
Text(stringResource(it.displayName)) Text(stringResource(it.displayName))

View File

@ -250,12 +250,17 @@ private fun PackageSelector(packages: Set<String>, onFinish: (String?) -> Unit)
} }
@Composable @Composable
private fun GroupItem(onClick: () -> Unit, @StringRes headline: Int, @StringRes description: Int) = private fun GroupItem(
onClick: () -> Unit,
@StringRes headline: Int,
@StringRes description: Int? = null
) {
SettingsListItem( SettingsListItem(
modifier = Modifier.clickable { onClick() }, modifier = Modifier.clickable { onClick() },
headlineContent = stringResource(headline), headlineContent = stringResource(headline),
supportingContent = stringResource(description) supportingContent = description?.let { stringResource(it) }
) )
}
@Composable @Composable
fun KeystoreCredentialsDialog( fun KeystoreCredentialsDialog(

View File

@ -1,8 +1,11 @@
package app.revanced.manager.ui.screen.settings package app.revanced.manager.ui.screen.settings
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.* import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource

View File

@ -79,7 +79,7 @@ class AppSelectorViewModel(
pm.getPackageInfo(this)?.let { packageInfo -> pm.getPackageInfo(this)?.let { packageInfo ->
SelectedApp.Local( SelectedApp.Local(
packageName = packageInfo.packageName, packageName = packageInfo.packageName,
version = packageInfo.versionName, version = packageInfo.versionName!!,
file = this, file = this,
temporary = true temporary = true
) )

View File

@ -103,13 +103,10 @@ class DashboardViewModel(
selectedSources.clear() selectedSources.clear()
} }
fun createLocalSource(patchBundle: Uri, integrations: Uri?) = fun createLocalSource(patchBundle: Uri) =
viewModelScope.launch { viewModelScope.launch {
contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> contentResolver.openInputStream(patchBundle)!!.use { patchesStream ->
integrations?.let { contentResolver.openInputStream(it) } patchBundleRepository.createLocal(patchesStream)
.use { integrationsStream ->
patchBundleRepository.createLocal(patchesStream, integrationsStream)
}
} }
} }

View File

@ -78,7 +78,7 @@ class InstalledAppInfoViewModel(
when (installedApp.installType) { when (installedApp.installType) {
InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName) InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName)
InstallType.ROOT -> viewModelScope.launch { InstallType.MOUNT -> viewModelScope.launch {
rootInstaller.uninstall(installedApp.currentPackageName) rootInstaller.uninstall(installedApp.currentPackageName)
installedAppRepository.delete(installedApp) installedAppRepository.delete(installedApp)
onBackClick() onBackClick()

View File

@ -30,7 +30,7 @@ class InstalledAppsViewModel(
packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) { packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) {
try { try {
if ( if (
installedApp.installType == InstallType.ROOT && !rootInstaller.isAppInstalled(installedApp.currentPackageName) installedApp.installType == InstallType.MOUNT && !rootInstaller.isAppInstalled(installedApp.currentPackageName)
) { ) {
installedAppsRepository.delete(installedApp) installedAppsRepository.delete(installedApp)
return@withContext null return@withContext null
@ -39,7 +39,7 @@ class InstalledAppsViewModel(
val packageInfo = pm.getPackageInfo(installedApp.currentPackageName) val packageInfo = pm.getPackageInfo(installedApp.currentPackageName)
if (packageInfo == null && installedApp.installType != InstallType.ROOT) { if (packageInfo == null && installedApp.installType != InstallType.MOUNT) {
installedAppsRepository.delete(installedApp) installedAppsRepository.delete(installedApp)
return@withContext null return@withContext null
} }

View File

@ -34,6 +34,8 @@ import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.service.InstallService import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService
import app.revanced.manager.ui.component.InstallerStatusDialogModel
import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.State
@ -74,6 +76,20 @@ class PatcherViewModel(
private val installedAppRepository: InstalledAppRepository by inject() private val installedAppRepository: InstalledAppRepository by inject()
private val rootInstaller: RootInstaller by inject() private val rootInstaller: RootInstaller by inject()
val installerStatusDialogModel : InstallerStatusDialogModel = object : InstallerStatusDialogModel {
override var packageInstallerStatus: Int? by mutableStateOf(null)
override fun reinstall() {
this@PatcherViewModel.reinstall()
}
override fun install() {
// Since this is a package installer status dialog,
// InstallType.MOUNT is never used here.
install(InstallType.DEFAULT)
}
}
private var installedApp: InstalledApp? = null private var installedApp: InstalledApp? = null
val packageName: String = input.selectedApp.packageName val packageName: String = input.selectedApp.packageName
var installedPackageName by mutableStateOf<String?>(null) var installedPackageName by mutableStateOf<String?>(null)
@ -179,20 +195,25 @@ class PatcherViewModel(
) )
val patcherSucceeded = val patcherSucceeded =
workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo -> workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo? ->
when (workInfo.state) { when (workInfo?.state) {
WorkInfo.State.SUCCEEDED -> true WorkInfo.State.SUCCEEDED -> true
WorkInfo.State.FAILED -> false WorkInfo.State.FAILED -> false
else -> null else -> null
} }
} }
private val installBroadcastReceiver = object : BroadcastReceiver() { private val installerBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) { when (intent?.action) {
InstallService.APP_INSTALL_ACTION -> { InstallService.APP_INSTALL_ACTION -> {
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999) val pmStatus = intent.getIntExtra(
val extra = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!! InstallService.EXTRA_INSTALL_STATUS,
PackageInstaller.STATUS_FAILURE
)
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
?.let(logger::trace)
if (pmStatus == PackageInstaller.STATUS_SUCCESS) { if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
app.toast(app.getString(R.string.install_app_success)) app.toast(app.getString(R.string.install_app_success))
@ -203,14 +224,29 @@ class PatcherViewModel(
installedPackageName!!, installedPackageName!!,
packageName, packageName,
input.selectedApp.version input.selectedApp.version
?: pm.getPackageInfo(outputFile)!!.versionName, ?: pm.getPackageInfo(outputFile)?.versionName!!,
InstallType.DEFAULT, InstallType.DEFAULT,
input.selectedPatches input.selectedPatches
) )
} }
} else { }
app.toast(app.getString(R.string.install_app_fail, extra))
Log.e(tag, "Installation failed: $extra") installerStatusDialogModel.packageInstallerStatus = pmStatus
isInstalling = false
}
UninstallService.APP_UNINSTALL_ACTION -> {
val pmStatus = intent.getIntExtra(
UninstallService.EXTRA_UNINSTALL_STATUS,
PackageInstaller.STATUS_FAILURE
)
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
?.let(logger::trace)
if (pmStatus != PackageInstaller.STATUS_SUCCESS) {
installerStatusDialogModel.packageInstallerStatus = pmStatus
} }
} }
} }
@ -218,9 +254,15 @@ class PatcherViewModel(
} }
init { // TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it. init { // TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it.
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply { ContextCompat.registerReceiver(
addAction(InstallService.APP_INSTALL_ACTION) app,
}, ContextCompat.RECEIVER_NOT_EXPORTED) installerBroadcastReceiver,
IntentFilter().apply {
addAction(InstallService.APP_INSTALL_ACTION)
addAction(UninstallService.APP_UNINSTALL_ACTION)
},
ContextCompat.RECEIVER_NOT_EXPORTED
)
viewModelScope.launch { viewModelScope.launch {
installedApp = installedAppRepository.get(packageName) installedApp = installedAppRepository.get(packageName)
@ -230,10 +272,10 @@ class PatcherViewModel(
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
app.unregisterReceiver(installBroadcastReceiver) app.unregisterReceiver(installerBroadcastReceiver)
workManager.cancelWorkById(patcherWorkerId) workManager.cancelWorkById(patcherWorkerId)
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.ROOT) { if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
uiSafe(app, R.string.failed_to_mount, "Failed to mount") { uiSafe(app, R.string.failed_to_mount, "Failed to mount") {
withTimeout(Duration.ofMinutes(1L)) { withTimeout(Duration.ofMinutes(1L)) {
@ -246,6 +288,8 @@ class PatcherViewModel(
tempDir.deleteRecursively() tempDir.deleteRecursively()
} }
fun isDeviceRooted() = rootInstaller.isDeviceRooted()
fun rejectInteraction() { fun rejectInteraction() {
currentInteractionRequest?.complete(false) currentInteractionRequest?.complete(false)
} }
@ -308,34 +352,71 @@ class PatcherViewModel(
fun open() = installedPackageName?.let(pm::launch) fun open() = installedPackageName?.let(pm::launch)
fun install(installType: InstallType) = viewModelScope.launch { fun install(installType: InstallType) = viewModelScope.launch {
var pmInstallStarted = false
try { try {
isInstalling = true isInstalling = true
val currentPackageInfo = pm.getPackageInfo(outputFile)
?: throw Exception("Failed to load application info")
// If the app is currently installed
val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName)
if (existingPackageInfo != null) {
// Check if the app version is less than the installed version
if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
// Exit if the selected app version is less than the installed version
installerStatusDialogModel.packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
return@launch
}
}
when (installType) { when (installType) {
InstallType.DEFAULT -> { InstallType.DEFAULT -> {
// Check if the app is mounted as root
// If it is, unmount it first, silently
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
rootInstaller.unmount(packageName)
}
// Install regularly
pm.installApp(listOf(outputFile)) pm.installApp(listOf(outputFile))
pmInstallStarted = true
} }
InstallType.ROOT -> { InstallType.MOUNT -> {
try { try {
val packageInfo = pm.getPackageInfo(outputFile) val packageInfo = pm.getPackageInfo(outputFile)
?: throw Exception("Failed to load application info") ?: throw Exception("Failed to load application info")
val label = with(pm) { val label = with(pm) {
packageInfo.label() packageInfo.label()
} }
// Check for base APK, first check if the app is already installed
if (existingPackageInfo == null) {
// If the app is not installed, check if the output file is a base apk
if (currentPackageInfo.splitNames.isNotEmpty()) {
// Exit if there is no base APK package
installerStatusDialogModel.packageInstallerStatus =
PackageInstaller.STATUS_FAILURE_INVALID
return@launch
}
}
// Install as root
rootInstaller.install( rootInstaller.install(
outputFile, outputFile,
inputFile, inputFile,
packageName, packageName,
packageInfo.versionName, // input.selectedApp.version?
packageInfo.versionName!!,
label label
) )
installedAppRepository.addOrUpdate( installedAppRepository.addOrUpdate(
packageName, packageName,
// TODO: this seems wrong
packageName, packageName,
packageInfo.versionName, packageInfo.versionName!!,
InstallType.ROOT, InstallType.MOUNT,
input.selectedPatches input.selectedPatches
) )
@ -354,8 +435,22 @@ class PatcherViewModel(
} }
} }
} }
} catch(e: Exception) {
Log.e(tag, "Failed to install", e)
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
} finally { } finally {
isInstalling = false if (!pmInstallStarted)
isInstalling = false
}
}
fun reinstall() = viewModelScope.launch {
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
?: throw Exception("Failed to load application info")
pm.installApp(listOf(outputFile))
isInstalling = true
} }
} }

View File

@ -100,7 +100,7 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
packageInfo.await()?.let { packageInfo.await()?.let {
SelectedApp.Installed( SelectedApp.Installed(
packageName, packageName,
it.versionName it.versionName!!
) to installedAppDeferred.await() ) to installedAppDeferred.await()
} }
} }

View File

@ -1,14 +1,8 @@
package app.revanced.manager.util package app.revanced.manager.util
private const val team = "revanced"
const val ghOrganization = "https://github.com/$team"
const val ghCli = "$team/revanced-cli"
const val ghPatches = "$team/revanced-patches"
const val ghPatcher = "$team/revanced-patcher"
const val ghManager = "$team/revanced-manager"
const val ghIntegrations = "$team/revanced-integrations"
const val tag = "ReVanced Manager" const val tag = "ReVanced Manager"
const val JAR_MIMETYPE = "application/java-archive" const val JAR_MIMETYPE = "application/java-archive"
const val APK_MIMETYPE = "application/vnd.android.package-archive" const val APK_MIMETYPE = "application/vnd.android.package-archive"
const val JSON_MIMETYPE = "application/json" const val JSON_MIMETYPE = "application/json"
const val BIN_MIMETYPE = "application/octet-stream"

View File

@ -10,11 +10,11 @@ import android.content.pm.PackageInstaller
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.PackageInfoFlags import android.content.pm.PackageManager.PackageInfoFlags
import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.PackageManager.NameNotFoundException
import androidx.core.content.pm.PackageInfoCompat
import android.content.pm.Signature import android.content.pm.Signature
import android.os.Build import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.core.content.pm.PackageInfoCompat
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.service.InstallService import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService import app.revanced.manager.service.UninstallService
@ -121,7 +121,7 @@ class PM(
val pkgInfo = app.packageManager.getPackageArchiveInfo(path, 0) ?: return null val pkgInfo = app.packageManager.getPackageArchiveInfo(path, 0) ?: return null
// This is needed in order to load label and icon. // This is needed in order to load label and icon.
pkgInfo.applicationInfo.apply { pkgInfo.applicationInfo!!.apply {
sourceDir = path sourceDir = path
publicSourceDir = path publicSourceDir = path
} }
@ -129,6 +129,10 @@ class PM(
return pkgInfo return pkgInfo
} }
fun PackageInfo.label() = this.applicationInfo!!.loadLabel(app.packageManager).toString()
fun getVersionCode(packageInfo: PackageInfo) = PackageInfoCompat.getLongVersionCode(packageInfo)
fun getSignature(packageName: String): Signature = fun getSignature(packageName: String): Signature =
// Get the last signature from the list because we want the newest one if SigningInfo.getSigningCertificateHistory() was used. // Get the last signature from the list because we want the newest one if SigningInfo.getSigningCertificateHistory() was used.
PackageInfoCompat.getSignatures(app.packageManager, packageName).last() PackageInfoCompat.getSignatures(app.packageManager, packageName).last()
@ -141,8 +145,6 @@ class PM(
false false
) )
fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString()
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) { suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
val packageInstaller = app.packageManager.packageInstaller val packageInstaller = app.packageManager.packageInstaller
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session -> packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
@ -198,4 +200,4 @@ class PM(
Intent(this, UninstallService::class.java), Intent(this, UninstallService::class.java),
intentFlags intentFlags
).intentSender ).intentSender
} }

View File

@ -2,6 +2,7 @@ package app.revanced.manager.util
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.Settings import android.provider.Settings
@ -10,7 +11,7 @@ import androidx.annotation.RequiresApi
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
class RequestManageStorageContract(private val forceLaunch: Boolean = false) : ActivityResultContract<String, Boolean>() { class RequestManageStorageContract(private val forceLaunch: Boolean = false) : ActivityResultContract<String, Boolean>() {
override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.fromParts("package", context.packageName, null))
override fun getSynchronousResult(context: Context, input: String): SynchronousResult<Boolean>? = if (!forceLaunch && Environment.isExternalStorageManager()) SynchronousResult(true) else null override fun getSynchronousResult(context: Context, input: String): SynchronousResult<Boolean>? = if (!forceLaunch && Environment.isExternalStorageManager()) SynchronousResult(true) else null

View File

@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.ListItemColors import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -27,6 +28,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@ -262,4 +264,24 @@ fun ScrollState.isScrollingUp(): State<Boolean> {
val LazyListState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value val LazyListState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
@Composable
@ReadOnlyComposable
fun <R> (() -> R).withHapticFeedback(constant: Int): () -> R {
val view = LocalView.current
return {
view.performHapticFeedback(constant)
this()
}
}
@Composable
@ReadOnlyComposable
fun <T, R> ((T) -> R).withHapticFeedback(constant: Int): (T) -> R {
val view = LocalView.current
return {
view.performHapticFeedback(constant)
this(it)
}
}
fun Modifier.enabled(condition: Boolean) = if (condition) this else alpha(0.5f) fun Modifier.enabled(condition: Boolean) = if (condition) this else alpha(0.5f)

View File

@ -11,8 +11,4 @@
<plurals name="selected_count"> <plurals name="selected_count">
<item quantity="other">%d selected</item> <item quantity="other">%d selected</item>
</plurals> </plurals>
<plurals name="bundle_patches_available">
<item quantity="one">%d patch available</item>
<item quantity="other">%d patches available</item>
</plurals>
</resources> </resources>

View File

@ -2,13 +2,15 @@
<string name="app_name">ReVanced Manager</string> <string name="app_name">ReVanced Manager</string>
<string name="patcher">Patcher</string> <string name="patcher">Patcher</string>
<string name="patches">Patches</string> <string name="patches">Patches</string>
<string name="integrations">Integrations</string>
<string name="cli">CLI</string> <string name="cli">CLI</string>
<string name="manager">Manager</string> <string name="manager">Manager</string>
<string name="plugin_host_permission_label">ReVanced Manager plugin host</string> <string name="plugin_host_permission_label">ReVanced Manager plugin host</string>
<string name="plugin_host_permission_description">Used to control access to ReVanced Manager plugins. Only ReVanced Manager has this.</string> <string name="plugin_host_permission_description">Used to control access to ReVanced Manager plugins. Only ReVanced Manager has this.</string>
<string name="toast_copied_to_clipboard">Copied!</string>
<string name="copy_to_clipboard">Copy to clipboard</string>
<string name="dashboard">Dashboard</string> <string name="dashboard">Dashboard</string>
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="select_app">Select an app</string> <string name="select_app">Select an app</string>
@ -21,7 +23,6 @@
<string name="import_bundle">Import patch bundle</string> <string name="import_bundle">Import patch bundle</string>
<string name="bundle_patches">Bundle patches</string> <string name="bundle_patches">Bundle patches</string>
<string name="patch_bundle_field">Patch bundle</string> <string name="patch_bundle_field">Patch bundle</string>
<string name="integrations_field">Integrations</string>
<string name="file_field_set">Selected</string> <string name="file_field_set">Selected</string>
<string name="file_field_not_set">Not selected</string> <string name="file_field_not_set">Not selected</string>
@ -56,19 +57,18 @@
<string name="auto_updates_dialog_note">These settings can be changed later.</string> <string name="auto_updates_dialog_note">These settings can be changed later.</string>
<string name="general">General</string> <string name="general">General</string>
<string name="general_description">General settings</string> <string name="general_description">Theme, dynamic color</string>
<string name="advanced">Advanced</string>
<string name="advanced_description">Advanced settings</string>
<string name="updates">Updates</string> <string name="updates">Updates</string>
<string name="updates_description">Updates for ReVanced Manager</string> <string name="updates_description">Check for updates and view changelogs</string>
<string name="downloads">Downloads</string>
<string name="downloads_description">Downloader plugins and downloaded apps</string>
<string name="import_export">Import &amp; export</string>
<string name="import_export_description">Keystore, patch options and selection</string>
<string name="advanced">Advanced</string>
<string name="advanced_description">API URL, memory limit, debugging</string>
<string name="about">About</string>
<string name="opensource_licenses">Open source licenses</string> <string name="opensource_licenses">Open source licenses</string>
<string name="opensource_licenses_description">View all the libraries used to make this application</string> <string name="opensource_licenses_description">View all the libraries used to make this application</string>
<string name="downloads">Downloads</string>
<string name="downloads_description">Manage downloaded content</string>
<string name="import_export">Import &amp; export</string>
<string name="import_export_description">Import and export settings</string>
<string name="about">About</string>
<string name="about_description">About ReVanced</string>
<string name="contributors">Contributors</string> <string name="contributors">Contributors</string>
<string name="contributors_description">View the contributors of ReVanced</string> <string name="contributors_description">View the contributors of ReVanced</string>
@ -76,8 +76,6 @@
<string name="dynamic_color_description">Adapt colors to the wallpaper</string> <string name="dynamic_color_description">Adapt colors to the wallpaper</string>
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="theme_description">Choose between light or dark theme</string> <string name="theme_description">Choose between light or dark theme</string>
<string name="multithreaded_dex_file_writer">Multi-threaded DEX file writer</string>
<string name="multithreaded_dex_file_writer_description">Use multiple cores to write DEX files. This is faster, but uses more memory</string>
<string name="safeguards">Safeguards</string> <string name="safeguards">Safeguards</string>
<string name="patch_compat_check">Disable version compatibility check</string> <string name="patch_compat_check">Disable version compatibility check</string>
<string name="patch_compat_check_description">The check restricts patches to supported app versions</string> <string name="patch_compat_check_description">The check restricts patches to supported app versions</string>
@ -132,7 +130,6 @@
<string name="search_apps">Search apps…</string> <string name="search_apps">Search apps…</string>
<string name="loading_body">Loading…</string> <string name="loading_body">Loading…</string>
<string name="downloading_patches">Downloading patch bundle…</string> <string name="downloading_patches">Downloading patch bundle…</string>
<string name="downloading_integrations">Downloading Integrations…</string>
<string name="options">Options</string> <string name="options">Options</string>
<string name="ok">OK</string> <string name="ok">OK</string>
@ -154,7 +151,6 @@
<string name="close">Close</string> <string name="close">Close</string>
<string name="system">System</string> <string name="system">System</string>
<string name="light">Light</string> <string name="light">Light</string>
<string name="information">Information</string>
<string name="dark">Dark</string> <string name="dark">Dark</string>
<string name="appearance">Appearance</string> <string name="appearance">Appearance</string>
<string name="downloaded_apps">Downloaded apps</string> <string name="downloaded_apps">Downloaded apps</string>
@ -180,7 +176,7 @@
<string name="device_memory_limit">Memory limits</string> <string name="device_memory_limit">Memory limits</string>
<string name="device_memory_limit_format">%1$dMB (Normal) - %2$dMB (Large)</string> <string name="device_memory_limit_format">%1$dMB (Normal) - %2$dMB (Large)</string>
<string name="patch_bundles_section">Patch bundles</string> <string name="patch_bundles_section">Patch bundles</string>
<string name="patch_bundles_redownload">Redownload all patch bundles</string> <string name="patch_bundles_force_download">Force download all patch bundles</string>
<string name="patch_bundles_reset">Reset patch bundles</string> <string name="patch_bundles_reset">Reset patch bundles</string>
<string name="patching">Patching</string> <string name="patching">Patching</string>
<string name="signing">Signing</string> <string name="signing">Signing</string>
@ -190,15 +186,12 @@
<string name="tab_bundles">Patch bundles</string> <string name="tab_bundles">Patch bundles</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="refresh">Refresh</string> <string name="refresh">Refresh</string>
<string name="remote">Remote</string>
<string name="local">Local</string>
<string name="continue_anyways">Continue anyways</string> <string name="continue_anyways">Continue anyways</string>
<string name="download_another_version">Download another version</string> <string name="download_another_version">Download another version</string>
<string name="download_app">Download app</string> <string name="download_app">Download app</string>
<string name="download_apk">Download APK file</string> <string name="download_apk">Download APK file</string>
<string name="source_download_fail">Failed to download patch bundle: %s</string> <string name="source_download_fail">Failed to download patch bundle: %s</string>
<string name="source_replace_fail">Failed to load updated patch bundle: %s</string> <string name="source_replace_fail">Failed to load updated patch bundle: %s</string>
<string name="source_replace_integrations_fail">Failed to update integrations: %s</string>
<string name="no_patched_apps_found">No patched apps found</string> <string name="no_patched_apps_found">No patched apps found</string>
<string name="tap_on_patches">Tap on the patches to get more information about them</string> <string name="tap_on_patches">Tap on the patches to get more information about them</string>
<string name="bundles_selected">%s selected</string> <string name="bundles_selected">%s selected</string>
@ -235,7 +228,7 @@
<string name="applied_patches">Applied patches</string> <string name="applied_patches">Applied patches</string>
<string name="view_applied_patches">View applied patches</string> <string name="view_applied_patches">View applied patches</string>
<string name="default_install">Default</string> <string name="default_install">Default</string>
<string name="root_install">Root</string> <string name="mount_install">Mount</string>
<string name="mounted">Mounted</string> <string name="mounted">Mounted</string>
<string name="not_mounted">Not mounted</string> <string name="not_mounted">Not mounted</string>
<string name="mount">Mount</string> <string name="mount">Mount</string>
@ -275,6 +268,7 @@
<string name="install_app">Install</string> <string name="install_app">Install</string>
<string name="install_app_success">App installed</string> <string name="install_app_success">App installed</string>
<string name="install_app_fail">Failed to install app: %s</string> <string name="install_app_fail">Failed to install app: %s</string>
<string name="reinstall_app_fail">Failed to reinstall app: %s</string>
<string name="uninstall_app_fail">Failed to uninstall app: %s</string> <string name="uninstall_app_fail">Failed to uninstall app: %s</string>
<string name="open_app">Open</string> <string name="open_app">Open</string>
<string name="save_apk">Save APK</string> <string name="save_apk">Save APK</string>
@ -305,6 +299,7 @@
<string name="drag_handle">reorder</string> <string name="drag_handle">reorder</string>
<string name="more">More</string> <string name="more">More</string>
<string name="less">Less</string>
<string name="continue_">Continue</string> <string name="continue_">Continue</string>
<string name="dismiss">Dismiss</string> <string name="dismiss">Dismiss</string>
<string name="permanent_dismiss">Do not show this again</string> <string name="permanent_dismiss">Do not show this again</string>
@ -317,14 +312,15 @@
<string name="submit_feedback_description">Help us improve this application</string> <string name="submit_feedback_description">Help us improve this application</string>
<string name="developer_options">Developer options</string> <string name="developer_options">Developer options</string>
<string name="developer_options_description">Options for debugging issues</string> <string name="developer_options_description">Options for debugging issues</string>
<string name="bundle_input_name">Name</string>
<string name="bundle_input_source_url">Source URL</string> <string name="bundle_input_source_url">Source URL</string>
<string name="bundle_update_success">Successfully updated %s</string> <string name="bundle_update_success">Successfully updated %s</string>
<string name="bundle_update_unavailable">No update available for %s</string> <string name="bundle_update_unavailable">No update available for %s</string>
<string name="bundle_auto_update">Auto update</string> <string name="bundle_auto_update">Auto update</string>
<string name="bundle_auto_update_description">Automatically update this bundle when ReVanced starts</string> <string name="bundle_auto_update_description">Automatically update this bundle when ReVanced starts</string>
<string name="bundle_type">Bundle type</string> <string name="bundle_view_patches">View patches</string>
<string name="bundle_type_description">Choose the type of bundle you want</string> <string name="bundle_view_patches_any_version">Any version</string>
<string name="bundle_view_patches_any_package">Any package</string>
<string name="about_revanced_manager">About ReVanced Manager</string> <string name="about_revanced_manager">About ReVanced Manager</string>
<string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string> <string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string>
<string name="update_available">An update is available</string> <string name="update_available">An update is available</string>
@ -376,6 +372,24 @@
<string name="local_bundle_description">Import local files from your storage, does not automatically update</string> <string name="local_bundle_description">Import local files from your storage, does not automatically update</string>
<string name="remote_bundle_description">Import remote files from a URL, can automatically update</string> <string name="remote_bundle_description">Import remote files from a URL, can automatically update</string>
<string name="recommended">Recommended</string> <string name="recommended">Recommended</string>
<string name="installation_failed_dialog_title">Installation failed</string>
<string name="installation_cancelled_dialog_title">Installation cancelled</string>
<string name="installation_blocked_dialog_title">Installation blocked</string>
<string name="installation_conflict_dialog_title">Installation conflict</string>
<string name="installation_incompatible_dialog_title">Installation incompatible</string>
<string name="installation_invalid_dialog_title">Installation invalid</string>
<string name="installation_storage_issue_dialog_title">Not enough storage</string>
<string name="installation_timeout_dialog_title">Installation timed out</string>
<string name="installation_failed_description">The installation failed due to an unknown reason. Try again?</string>
<string name="installation_aborted_description">The installation was cancelled manually. Try again?</string>
<string name="installation_blocked_description">The installation was blocked. Review your device security settings and try again.</string>
<string name="installation_conflict_description">The installation was prevented by an existing installation of the app. Uninstall the installed app and try again?</string>
<string name="installation_incompatible_description">The app is incompatible with this device. Use an APK that is supported by this device and try again.</string>
<string name="installation_invalid_description">The app is invalid. Uninstall the app and try again?</string>
<string name="installation_storage_issue_description">The app could not be installed due to insufficient storage. Free up some space and try again.</string>
<string name="installation_timeout_description">The installation took too long. Try again?</string>
<string name="reinstall">Reinstall</string>
<string name="show">Show</string> <string name="show">Show</string>
<string name="debugging">Debugging</string> <string name="debugging">Debugging</string>
<string name="about_device">About device</string> <string name="about_device">About device</string>

View File

@ -1,11 +1,11 @@
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.devtools) apply false alias(libs.plugins.devtools) apply false
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.about.libraries) apply false alias(libs.plugins.about.libraries) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.binary.compatibility.validator) alias(libs.plugins.binary.compatibility.validator)
} }

View File

@ -1,30 +1,30 @@
[versions] [versions]
kotlin = "2.0.10" ktx = "1.15.0"
ktx = "1.13.1" material3 = "1.3.1"
material3 = "1.3.0-beta05" ui-tooling = "1.7.5"
ui-tooling = "1.6.8" viewmodel-lifecycle = "2.8.7"
viewmodel-lifecycle = "2.8.4"
splash-screen = "1.0.1" splash-screen = "1.0.1"
compose-activity = "1.9.1" compose-activity = "1.9.3"
preferences-datastore = "1.1.1" preferences-datastore = "1.1.1"
work-runtime = "2.9.1" work-runtime = "2.10.0"
compose-bom = "2024.06.00" compose-bom = "2024.11.00"
accompanist = "0.34.0" accompanist = "0.34.0"
placeholder = "1.1.2" placeholder = "1.1.2"
reorderable = "1.5.2" reorderable = "1.5.2"
serialization = "1.7.1" serialization = "1.7.3"
collection = "0.3.7" collection = "0.3.8"
room-version = "2.6.1" room-version = "2.6.1"
revanced-patcher = "19.3.1" revanced-patcher = "21.0.0"
revanced-library = "2.2.1" revanced-library = "3.0.2"
koin-version = "3.5.3" koin-version = "3.5.3"
koin-version-compose = "3.5.3" koin-version-compose = "3.5.3"
reimagined-navigation = "1.5.0" reimagined-navigation = "1.5.0"
ktor = "2.3.9" ktor = "2.3.9"
markdown-renderer = "0.22.0" markdown-renderer = "0.22.0"
fading-edges = "1.0.4" fading-edges = "1.0.4"
android-gradle-plugin = "8.3.2" kotlin = "2.0.21"
dev-tools-ksp-gradle-plugin = "2.0.10-1.0.24" android-gradle-plugin = "8.7.2"
dev-tools-gradle-plugin = "2.0.21-1.0.27"
about-libraries-gradle-plugin = "11.1.1" about-libraries-gradle-plugin = "11.1.1"
binary-compatibility-validator = "0.15.1" binary-compatibility-validator = "0.15.1"
coil = "2.6.0" coil = "2.6.0"
@ -32,6 +32,7 @@ app-icon-loader-coil = "1.5.0"
skrapeit = "1.2.2" skrapeit = "1.2.2"
libsu = "5.2.2" libsu = "5.2.2"
scrollbars = "1.0.4" scrollbars = "1.0.4"
enumutil = "1.1.0"
compose-icons = "1.2.4" compose-icons = "1.2.4"
kotlin-process = "1.4.1" kotlin-process = "1.4.1"
hidden-api-stub = "4.3.3" hidden-api-stub = "4.3.3"
@ -126,6 +127,10 @@ libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref =
# Scrollbars # Scrollbars
scrollbars = { group = "com.github.GIGAMOLE", name = "ComposeScrollbars", version.ref = "scrollbars" } scrollbars = { group = "com.github.GIGAMOLE", name = "ComposeScrollbars", version.ref = "scrollbars" }
# EnumUtil
enumutil = { group = "io.github.materiiapps", name = "enumutil", version.ref = "enumutil" }
enumutil-ksp = { group = "io.github.materiiapps", name = "enumutil-ksp", version.ref = "enumutil" }
# Reorderable lists # Reorderable lists
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" } reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
@ -146,6 +151,6 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-ksp-gradle-plugin" } devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" }
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" } about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" }
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }

View File

@ -1,6 +1,7 @@
#Tue Nov 12 21:36:50 CET 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME