mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-04-30 22:14:25 +02:00
Merge branch 'compose-dev' into compose/downloader-system
This commit is contained in:
commit
3029a61e99
@ -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)
|
||||||
|
|
||||||
|
@ -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')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
)
|
)
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
|
@ -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)
|
@ -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 })
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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) },
|
||||||
|
@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
@ -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)
|
||||||
|
@ -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()) {
|
||||||
|
@ -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
|
||||||
)
|
)
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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"
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -116,7 +116,6 @@ fun AppSelectorScreen(
|
|||||||
},
|
},
|
||||||
colors = transparentListItemColors
|
colors = transparentListItemColors
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -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)) },
|
||||||
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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) }
|
||||||
)
|
)
|
||||||
|
@ -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))
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
@ -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 ->
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
@ -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>
|
@ -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 & 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 & 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>
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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" }
|
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user