mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2025-07-18 00:49:50 +08:00
Compare commits
22 Commits
compose/bu
...
feat/namin
Author | SHA1 | Date | |
---|---|---|---|
3fc2bc611c | |||
df04c67a36 | |||
486ed5967f | |||
789f9ec867 | |||
b51d1ee47a | |||
7148ee66f8 | |||
e1c4166a06 | |||
f7a4ae5791 | |||
cb2dbbee24 | |||
578dcce9b6 | |||
8c6c0f3c76 | |||
979a2dc410 | |||
baa9122a88 | |||
b70fc03bc7 | |||
81a4ebd327 | |||
83fc7f131a | |||
8e4a9088ea | |||
7ee2b1a026 | |||
40c99ab4dc | |||
1752fae9d9 | |||
d264a2a363 | |||
a5e909cfc8 |
74
app/src/main/java/app/revanced/manager/data/redux/Redux.kt
Normal file
74
app/src/main/java/app/revanced/manager/data/redux/Redux.kt
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package app.revanced.manager.data.redux
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
|
||||||
|
// This file implements React Redux-like state management.
|
||||||
|
|
||||||
|
class Store<S>(private val coroutineScope: CoroutineScope, initialState: S) : ActionContext {
|
||||||
|
private val _state = MutableStateFlow(initialState)
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
// Do not touch these without the lock.
|
||||||
|
private var isRunningActions = false
|
||||||
|
private val queueChannel = Channel<Action<S>>(capacity = 10)
|
||||||
|
private val lock = Mutex()
|
||||||
|
|
||||||
|
suspend fun dispatch(action: Action<S>) = lock.withLock {
|
||||||
|
Log.d(tag, "Dispatching $action")
|
||||||
|
queueChannel.send(action)
|
||||||
|
|
||||||
|
if (isRunningActions) return@withLock
|
||||||
|
isRunningActions = true
|
||||||
|
coroutineScope.launch {
|
||||||
|
runActions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private suspend fun runActions() {
|
||||||
|
while (true) {
|
||||||
|
val action = withTimeoutOrNull(200L) { queueChannel.receive() }
|
||||||
|
if (action == null) {
|
||||||
|
Log.d(tag, "Stopping action runner")
|
||||||
|
lock.withLock {
|
||||||
|
// New actions may be dispatched during the timeout.
|
||||||
|
isRunningActions = !queueChannel.isEmpty
|
||||||
|
if (!isRunningActions) return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(tag, "Running $action")
|
||||||
|
_state.value = try {
|
||||||
|
with(action) { this@Store.execute(_state.value) }
|
||||||
|
} catch (c: CancellationException) {
|
||||||
|
// This is done without the lock, but cancellation usually means the store is no longer needed.
|
||||||
|
isRunningActions = false
|
||||||
|
throw c
|
||||||
|
} catch (e: Exception) {
|
||||||
|
action.catch(e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionContext
|
||||||
|
|
||||||
|
interface Action<S> {
|
||||||
|
suspend fun ActionContext.execute(current: S): S
|
||||||
|
suspend fun catch(exception: Exception) {
|
||||||
|
Log.e(tag, "Got exception while executing $this", exception)
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +1,15 @@
|
|||||||
package app.revanced.manager.data.room.bundles
|
package app.revanced.manager.data.room.bundles
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface PatchBundleDao {
|
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, auto_update FROM patch_bundles WHERE uid = :uid")
|
|
||||||
fun getPropsById(uid: Int): Flow<BundleProperties?>
|
|
||||||
|
|
||||||
@Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
|
@Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
|
||||||
suspend fun updateVersionHash(uid: Int, patches: String?)
|
suspend fun updateVersionHash(uid: Int, patches: String?)
|
||||||
|
|
||||||
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
|
|
||||||
suspend fun setAutoUpdate(uid: Int, value: Boolean)
|
|
||||||
|
|
||||||
@Query("UPDATE patch_bundles SET name = :value WHERE uid = :uid")
|
|
||||||
suspend fun setName(uid: Int, value: String)
|
|
||||||
|
|
||||||
@Query("DELETE FROM patch_bundles WHERE uid != 0")
|
@Query("DELETE FROM patch_bundles WHERE uid != 0")
|
||||||
suspend fun purgeCustomBundles()
|
suspend fun purgeCustomBundles()
|
||||||
|
|
||||||
@ -32,6 +22,9 @@ interface PatchBundleDao {
|
|||||||
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
|
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
|
||||||
suspend fun remove(uid: Int)
|
suspend fun remove(uid: Int)
|
||||||
|
|
||||||
@Insert
|
@Query("SELECT name, version, auto_update, source FROM patch_bundles WHERE uid = :uid")
|
||||||
suspend fun add(source: PatchBundleEntity)
|
suspend fun getProps(uid: Int): PatchBundleProperties?
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
suspend fun upsert(source: PatchBundleEntity)
|
||||||
}
|
}
|
@ -38,7 +38,9 @@ data class PatchBundleEntity(
|
|||||||
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BundleProperties(
|
data class PatchBundleProperties(
|
||||||
|
@ColumnInfo(name = "name") val name: String,
|
||||||
@ColumnInfo(name = "version") val versionHash: String? = null,
|
@ColumnInfo(name = "version") val versionHash: String? = null,
|
||||||
|
@ColumnInfo(name = "source") val source: Source,
|
||||||
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||||
)
|
)
|
@ -15,7 +15,6 @@ val repositoryModule = module {
|
|||||||
createdAtStart()
|
createdAtStart()
|
||||||
}
|
}
|
||||||
singleOf(::NetworkInfo)
|
singleOf(::NetworkInfo)
|
||||||
singleOf(::PatchBundlePersistenceRepository)
|
|
||||||
singleOf(::PatchSelectionRepository)
|
singleOf(::PatchSelectionRepository)
|
||||||
singleOf(::PatchOptionsRepository)
|
singleOf(::PatchOptionsRepository)
|
||||||
singleOf(::PatchBundleRepository) {
|
singleOf(::PatchBundleRepository) {
|
||||||
|
@ -23,4 +23,5 @@ val viewModelModule = module {
|
|||||||
viewModelOf(::InstalledAppsViewModel)
|
viewModelOf(::InstalledAppsViewModel)
|
||||||
viewModelOf(::InstalledAppInfoViewModel)
|
viewModelOf(::InstalledAppInfoViewModel)
|
||||||
viewModelOf(::UpdatesSettingsViewModel)
|
viewModelOf(::UpdatesSettingsViewModel)
|
||||||
|
viewModelOf(::BundleListViewModel)
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,29 @@
|
|||||||
package app.revanced.manager.domain.bundles
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
|
import app.revanced.manager.data.redux.ActionContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
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
|
||||||
|
|
||||||
class LocalPatchBundle(name: String, id: Int, directory: File) :
|
class LocalPatchBundle(
|
||||||
PatchBundleSource(name, id, directory) {
|
name: String,
|
||||||
suspend fun replace(patches: InputStream) {
|
uid: Int,
|
||||||
|
error: Throwable?,
|
||||||
|
directory: File
|
||||||
|
) : PatchBundleSource(name, uid, error, directory) {
|
||||||
|
suspend fun ActionContext.replace(patches: InputStream) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
patchBundleOutputStream().use { outputStream ->
|
patchBundleOutputStream().use { outputStream ->
|
||||||
patches.copyTo(outputStream)
|
patches.copyTo(outputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
reload()?.also {
|
override fun copy(error: Throwable?, name: String) = LocalPatchBundle(
|
||||||
saveVersionHash(it.readManifestAttribute("Version"))
|
name,
|
||||||
}
|
uid,
|
||||||
}
|
error,
|
||||||
|
directory
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,10 @@
|
|||||||
package app.revanced.manager.domain.bundles
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import app.revanced.manager.data.redux.ActionContext
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
|
|
||||||
import app.revanced.manager.patcher.patch.PatchBundle
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
import app.revanced.manager.util.tag
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
import org.koin.core.component.inject
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
@ -24,27 +12,32 @@ import java.io.OutputStream
|
|||||||
* A [PatchBundle] source.
|
* A [PatchBundle] source.
|
||||||
*/
|
*/
|
||||||
@Stable
|
@Stable
|
||||||
sealed class PatchBundleSource(initialName: String, val uid: Int, directory: File) : KoinComponent {
|
sealed class PatchBundleSource(
|
||||||
protected val configRepository: PatchBundlePersistenceRepository by inject()
|
val name: String,
|
||||||
private val app: Application by inject()
|
val uid: Int,
|
||||||
|
error: Throwable?,
|
||||||
|
protected val directory: File
|
||||||
|
) {
|
||||||
protected val patchesFile = directory.resolve("patches.jar")
|
protected val patchesFile = directory.resolve("patches.jar")
|
||||||
|
|
||||||
private val _state = MutableStateFlow(load())
|
val state = when {
|
||||||
val state = _state.asStateFlow()
|
error != null -> State.Failed(error)
|
||||||
|
!hasInstalled() -> State.Missing
|
||||||
|
else -> State.Available(PatchBundle(patchesFile.absolutePath))
|
||||||
|
}
|
||||||
|
|
||||||
private val _nameFlow = MutableStateFlow(initialName)
|
val patchBundle get() = (state as? State.Available)?.bundle
|
||||||
val nameFlow =
|
val version get() = patchBundle?.manifestAttributes?.version
|
||||||
_nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.bundle_name_default else R.string.bundle_name_fallback) } }
|
val isNameOutOfDate get() = patchBundle?.manifestAttributes?.name?.let { it != name } == true
|
||||||
|
val error get() = (state as? State.Failed)?.throwable
|
||||||
|
|
||||||
suspend fun getName() = nameFlow.first()
|
suspend fun ActionContext.deleteLocalFile() = withContext(Dispatchers.IO) {
|
||||||
|
patchesFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
val versionFlow = state.map { it.patchBundleOrNull()?.readManifestAttribute("Version") }
|
abstract fun copy(error: Throwable? = this.error, name: String = this.name): PatchBundleSource
|
||||||
val patchCountFlow = state.map { it.patchBundleOrNull()?.patches?.size ?: 0 }
|
|
||||||
|
|
||||||
/**
|
protected fun hasInstalled() = patchesFile.exists()
|
||||||
* Returns true if the bundle has been downloaded to local storage.
|
|
||||||
*/
|
|
||||||
fun hasInstalled() = patchesFile.exists()
|
|
||||||
|
|
||||||
protected fun patchBundleOutputStream(): OutputStream = with(patchesFile) {
|
protected fun patchBundleOutputStream(): OutputStream = with(patchesFile) {
|
||||||
// Android 14+ requires dex containers to be readonly.
|
// Android 14+ requires dex containers to be readonly.
|
||||||
@ -56,62 +49,14 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun load(): State {
|
|
||||||
if (!hasInstalled()) return State.Missing
|
|
||||||
|
|
||||||
return try {
|
|
||||||
State.Loaded(PatchBundle(patchesFile))
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Log.e(tag, "Failed to load patch bundle with UID $uid", t)
|
|
||||||
State.Failed(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun reload(): PatchBundle? {
|
|
||||||
val newState = load()
|
|
||||||
_state.value = newState
|
|
||||||
|
|
||||||
val bundle = newState.patchBundleOrNull()
|
|
||||||
// Try to read the name from the patch bundle manifest if the bundle does not have a name.
|
|
||||||
if (bundle != null && _nameFlow.value.isEmpty()) {
|
|
||||||
bundle.readManifestAttribute("Name")?.let { setName(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return bundle
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a flow that emits the [app.revanced.manager.data.room.bundles.BundleProperties] of this [PatchBundleSource].
|
|
||||||
* The flow will emit null if the associated [PatchBundleSource] is deleted.
|
|
||||||
*/
|
|
||||||
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
|
|
||||||
suspend fun getProps() = propsFlow().first()!!
|
|
||||||
|
|
||||||
suspend fun currentVersionHash() = getProps().versionHash
|
|
||||||
protected suspend fun saveVersionHash(version: String?) =
|
|
||||||
configRepository.updateVersionHash(uid, version)
|
|
||||||
|
|
||||||
suspend fun setName(name: String) {
|
|
||||||
configRepository.setName(uid, name)
|
|
||||||
_nameFlow.value = name
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface State {
|
sealed interface State {
|
||||||
fun patchBundleOrNull(): PatchBundle? = null
|
|
||||||
|
|
||||||
data object Missing : State
|
data object Missing : State
|
||||||
data class Failed(val throwable: Throwable) : State
|
data class Failed(val throwable: Throwable) : State
|
||||||
data class Loaded(val bundle: PatchBundle) : State {
|
data class Available(val bundle: PatchBundle) : State
|
||||||
override fun patchBundleOrNull() = bundle
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object Extensions {
|
companion object Extensions {
|
||||||
val PatchBundleSource.isDefault inline get() = uid == 0
|
val PatchBundleSource.isDefault inline get() = uid == 0
|
||||||
val PatchBundleSource.asRemoteOrNull inline get() = this as? RemotePatchBundle
|
val PatchBundleSource.asRemoteOrNull inline get() = this as? RemotePatchBundle
|
||||||
val PatchBundleSource.nameState
|
|
||||||
@Composable inline get() = nameFlow.collectAsStateWithLifecycle(
|
|
||||||
""
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
package app.revanced.manager.domain.bundles
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import app.revanced.manager.data.redux.ActionContext
|
||||||
import app.revanced.manager.network.api.ReVancedAPI
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
import app.revanced.manager.network.dto.ReVancedAsset
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
import app.revanced.manager.network.service.HttpService
|
import app.revanced.manager.network.service.HttpService
|
||||||
@ -8,15 +8,24 @@ import app.revanced.manager.network.utils.getOrThrow
|
|||||||
import io.ktor.client.request.url
|
import io.ktor.client.request.url
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@Stable
|
sealed class RemotePatchBundle(
|
||||||
sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) :
|
name: String,
|
||||||
PatchBundleSource(name, id, directory) {
|
uid: Int,
|
||||||
|
protected val versionHash: String?,
|
||||||
|
error: Throwable?,
|
||||||
|
directory: File,
|
||||||
|
val endpoint: String,
|
||||||
|
val autoUpdate: Boolean,
|
||||||
|
) : PatchBundleSource(name, uid, error, directory), KoinComponent {
|
||||||
protected val http: HttpService by inject()
|
protected val http: HttpService by inject()
|
||||||
|
|
||||||
protected abstract suspend fun getLatestInfo(): ReVancedAsset
|
protected abstract suspend fun getLatestInfo(): ReVancedAsset
|
||||||
|
abstract fun copy(error: Throwable? = this.error, name: String = this.name, autoUpdate: Boolean = this.autoUpdate): RemotePatchBundle
|
||||||
|
override fun copy(error: Throwable?, name: String): RemotePatchBundle = copy(error, name, this.autoUpdate)
|
||||||
|
|
||||||
private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
|
private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
|
||||||
patchBundleOutputStream().use {
|
patchBundleOutputStream().use {
|
||||||
@ -25,47 +34,72 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveVersionHash(info.version)
|
info.version
|
||||||
reload()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadLatest() {
|
/**
|
||||||
download(getLatestInfo())
|
* Downloads the latest version regardless if there is a new update available.
|
||||||
}
|
*/
|
||||||
|
suspend fun ActionContext.downloadLatest() = download(getLatestInfo())
|
||||||
|
|
||||||
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
|
suspend fun ActionContext.update(): String? = withContext(Dispatchers.IO) {
|
||||||
val info = getLatestInfo()
|
val info = getLatestInfo()
|
||||||
if (hasInstalled() && info.version == currentVersionHash())
|
if (hasInstalled() && info.version == versionHash)
|
||||||
return@withContext false
|
return@withContext null
|
||||||
|
|
||||||
download(info)
|
download(info)
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
|
|
||||||
patchesFile.delete()
|
|
||||||
reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val updateFailMsg = "Failed to update patch bundle(s)"
|
const val updateFailMsg = "Failed to update patches"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
class JsonPatchBundle(
|
||||||
RemotePatchBundle(name, id, directory, endpoint) {
|
name: String,
|
||||||
|
uid: Int,
|
||||||
|
versionHash: String?,
|
||||||
|
error: Throwable?,
|
||||||
|
directory: File,
|
||||||
|
endpoint: String,
|
||||||
|
autoUpdate: Boolean,
|
||||||
|
) : RemotePatchBundle(name, uid, versionHash, error, directory, endpoint, autoUpdate) {
|
||||||
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
|
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
|
||||||
http.request<ReVancedAsset> {
|
http.request<ReVancedAsset> {
|
||||||
url(endpoint)
|
url(endpoint)
|
||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun copy(error: Throwable?, name: String, autoUpdate: Boolean) = JsonPatchBundle(
|
||||||
|
name,
|
||||||
|
uid,
|
||||||
|
versionHash,
|
||||||
|
error,
|
||||||
|
directory,
|
||||||
|
endpoint,
|
||||||
|
autoUpdate,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
class APIPatchBundle(
|
||||||
RemotePatchBundle(name, id, directory, endpoint) {
|
name: String,
|
||||||
|
uid: Int,
|
||||||
|
versionHash: String?,
|
||||||
|
error: Throwable?,
|
||||||
|
directory: File,
|
||||||
|
endpoint: String,
|
||||||
|
autoUpdate: Boolean,
|
||||||
|
) : RemotePatchBundle(name, uid, versionHash, error, directory, endpoint, autoUpdate) {
|
||||||
private val api: ReVancedAPI by inject()
|
private val api: ReVancedAPI by inject()
|
||||||
|
|
||||||
override suspend fun getLatestInfo() = api.getPatchesUpdate().getOrThrow()
|
override suspend fun getLatestInfo() = api.getPatchesUpdate().getOrThrow()
|
||||||
|
override fun copy(error: Throwable?, name: String, autoUpdate: Boolean) = APIPatchBundle(
|
||||||
|
name,
|
||||||
|
uid,
|
||||||
|
versionHash,
|
||||||
|
error,
|
||||||
|
directory,
|
||||||
|
endpoint,
|
||||||
|
autoUpdate,
|
||||||
|
)
|
||||||
}
|
}
|
@ -40,6 +40,8 @@ class DownloadedAppRepository(
|
|||||||
data: Parcelable,
|
data: Parcelable,
|
||||||
expectedPackageName: String,
|
expectedPackageName: String,
|
||||||
expectedVersion: String?,
|
expectedVersion: String?,
|
||||||
|
appCompatibilityCheck: Boolean,
|
||||||
|
patchesCompatibilityCheck: Boolean,
|
||||||
onDownload: suspend (downloadProgress: Pair<Long, Long?>) -> Unit,
|
onDownload: suspend (downloadProgress: Pair<Long, Long?>) -> Unit,
|
||||||
): File {
|
): File {
|
||||||
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
|
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
|
||||||
@ -96,7 +98,12 @@ class DownloadedAppRepository(
|
|||||||
val pkgInfo =
|
val pkgInfo =
|
||||||
pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid")
|
pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid")
|
||||||
if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}")
|
if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}")
|
||||||
if (expectedVersion != null && pkgInfo.versionName != expectedVersion) error("Downloaded APK has the wrong version. Expected: $expectedVersion, Actual: ${pkgInfo.versionName}")
|
expectedVersion?.let {
|
||||||
|
if (
|
||||||
|
pkgInfo.versionName != expectedVersion &&
|
||||||
|
(appCompatibilityCheck || patchesCompatibilityCheck)
|
||||||
|
) error("The selected app version ($pkgInfo.versionName) doesn't match the suggested version. Please use the suggested version ($expectedVersion), or adjust your settings by disabling \"Require suggested app version\" and enabling \"Disable version compatibility check\".")
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the previous copy (if present).
|
// Delete the previous copy (if present).
|
||||||
dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let {
|
dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let {
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
package app.revanced.manager.domain.repository
|
|
||||||
|
|
||||||
import app.revanced.manager.data.room.AppDatabase
|
|
||||||
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
|
||||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
|
||||||
import app.revanced.manager.data.room.bundles.Source
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
|
|
||||||
class PatchBundlePersistenceRepository(db: AppDatabase) {
|
|
||||||
private val dao = db.patchBundleDao()
|
|
||||||
|
|
||||||
suspend fun loadConfiguration(): List<PatchBundleEntity> {
|
|
||||||
val all = dao.all()
|
|
||||||
if (all.isEmpty()) {
|
|
||||||
dao.add(defaultSource)
|
|
||||||
return listOf(defaultSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
return all
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun reset() = dao.reset()
|
|
||||||
|
|
||||||
suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) =
|
|
||||||
PatchBundleEntity(
|
|
||||||
uid = generateUid(),
|
|
||||||
name = name,
|
|
||||||
versionHash = null,
|
|
||||||
source = source,
|
|
||||||
autoUpdate = autoUpdate
|
|
||||||
).also {
|
|
||||||
dao.add(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun delete(uid: Int) = dao.remove(uid)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the version hash used for updates.
|
|
||||||
*/
|
|
||||||
suspend fun updateVersionHash(uid: Int, versionHash: String?) =
|
|
||||||
dao.updateVersionHash(uid, versionHash)
|
|
||||||
|
|
||||||
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
|
|
||||||
|
|
||||||
suspend fun setName(uid: Int, name: String) = dao.setName(uid, name)
|
|
||||||
|
|
||||||
fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged()
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
val defaultSource = PatchBundleEntity(
|
|
||||||
uid = 0,
|
|
||||||
name = "",
|
|
||||||
versionHash = null,
|
|
||||||
source = Source.API,
|
|
||||||
autoUpdate = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,55 +3,78 @@ 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 androidx.annotation.StringRes
|
||||||
import app.revanced.library.mostCommonCompatibleVersions
|
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.redux.Action
|
||||||
|
import app.revanced.manager.data.redux.ActionContext
|
||||||
|
import app.revanced.manager.data.redux.Store
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
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.PatchBundleProperties
|
||||||
|
import app.revanced.manager.data.room.bundles.Source
|
||||||
import app.revanced.manager.domain.bundles.APIPatchBundle
|
import app.revanced.manager.domain.bundles.APIPatchBundle
|
||||||
import app.revanced.manager.domain.bundles.JsonPatchBundle
|
import app.revanced.manager.domain.bundles.JsonPatchBundle
|
||||||
import app.revanced.manager.data.room.bundles.Source as SourceInfo
|
import app.revanced.manager.data.room.bundles.Source as SourceInfo
|
||||||
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
||||||
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.util.flatMapLatestAndCombine
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||||
|
import app.revanced.manager.util.simpleMessage
|
||||||
import app.revanced.manager.util.tag
|
import app.revanced.manager.util.tag
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.toast
|
||||||
|
import kotlinx.collections.immutable.*
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import kotlin.collections.joinToString
|
||||||
|
import kotlin.collections.map
|
||||||
|
import kotlin.text.ifEmpty
|
||||||
|
|
||||||
class PatchBundleRepository(
|
class PatchBundleRepository(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val persistenceRepo: PatchBundlePersistenceRepository,
|
|
||||||
private val networkInfo: NetworkInfo,
|
private val networkInfo: NetworkInfo,
|
||||||
private val prefs: PreferencesManager,
|
private val prefs: PreferencesManager,
|
||||||
|
db: AppDatabase,
|
||||||
) {
|
) {
|
||||||
|
private val dao = db.patchBundleDao()
|
||||||
private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)
|
private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
private val _sources: MutableStateFlow<Map<Int, PatchBundleSource>> =
|
private val store = Store(CoroutineScope(Dispatchers.Default), State())
|
||||||
MutableStateFlow(emptyMap())
|
|
||||||
val sources = _sources.map { it.values.toList() }
|
|
||||||
|
|
||||||
val bundles = sources.flatMapLatestAndCombine(
|
val sources = store.state.map { it.sources.values.toList() }
|
||||||
combiner = {
|
val bundles = store.state.map {
|
||||||
it.mapNotNull { (uid, state) ->
|
it.sources.mapNotNull { (uid, src) ->
|
||||||
val bundle = state.patchBundleOrNull() ?: return@mapNotNull null
|
uid to (src.patchBundle ?: return@mapNotNull null)
|
||||||
uid to bundle
|
|
||||||
}.toMap()
|
}.toMap()
|
||||||
}
|
}
|
||||||
) {
|
val bundleInfoFlow = store.state.map { it.info }
|
||||||
it.state.map { state -> it.uid to state }
|
|
||||||
|
fun scopedBundleInfoFlow(packageName: String, version: String?) = bundleInfoFlow.map {
|
||||||
|
it.map { (_, bundleInfo) ->
|
||||||
|
bundleInfo.forPackage(
|
||||||
|
packageName,
|
||||||
|
version
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val suggestedVersions = bundles.map {
|
val patchCountsFlow = bundleInfoFlow.map { it.mapValues { (_, info) -> info.patches.size } }
|
||||||
|
|
||||||
|
val suggestedVersions = bundleInfoFlow.map {
|
||||||
val allPatches =
|
val allPatches =
|
||||||
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
||||||
|
|
||||||
@ -74,6 +97,100 @@ class PatchBundleRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend inline fun dispatchAction(
|
||||||
|
name: String,
|
||||||
|
crossinline block: suspend ActionContext.(current: State) -> State
|
||||||
|
) {
|
||||||
|
store.dispatch(object : Action<State> {
|
||||||
|
override suspend fun ActionContext.execute(current: State) = block(current)
|
||||||
|
override fun toString() = name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a reload. Do not call this outside of a store action.
|
||||||
|
*/
|
||||||
|
private suspend fun doReload(): State {
|
||||||
|
val entities = loadFromDb().onEach {
|
||||||
|
Log.d(tag, "Bundle: $it")
|
||||||
|
}
|
||||||
|
|
||||||
|
val sources = entities.associate { it.uid to it.load() }.toPersistentMap()
|
||||||
|
|
||||||
|
val hasOutOfDateNames = sources.values.any { it.isNameOutOfDate }
|
||||||
|
if (hasOutOfDateNames) dispatchAction(
|
||||||
|
"Sync names"
|
||||||
|
) { state ->
|
||||||
|
val nameChanges = state.sources.mapNotNull { (_, src) ->
|
||||||
|
if (!src.isNameOutOfDate) return@mapNotNull null
|
||||||
|
val newName = src.patchBundle?.manifestAttributes?.name?.takeIf { it != src.name }
|
||||||
|
?: return@mapNotNull null
|
||||||
|
|
||||||
|
src.uid to newName
|
||||||
|
}
|
||||||
|
val sources = state.sources.toMutableMap()
|
||||||
|
val info = state.info.toMutableMap()
|
||||||
|
nameChanges.forEach { (uid, name) ->
|
||||||
|
updateDb(uid) { it.copy(name = name) }
|
||||||
|
sources[uid] = sources[uid]!!.copy(name = name)
|
||||||
|
info[uid] = info[uid]?.copy(name = name) ?: return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
State(sources.toPersistentMap(), info.toPersistentMap())
|
||||||
|
}
|
||||||
|
val info = loadMetadata(sources).toPersistentMap()
|
||||||
|
|
||||||
|
return State(sources, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reload() = dispatchAction("Full reload") {
|
||||||
|
doReload()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadFromDb(): List<PatchBundleEntity> {
|
||||||
|
val all = dao.all()
|
||||||
|
if (all.isEmpty()) {
|
||||||
|
dao.upsert(defaultSource)
|
||||||
|
return listOf(defaultSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadMetadata(sources: Map<Int, PatchBundleSource>): Map<Int, PatchBundleInfo.Global> {
|
||||||
|
// Map bundles -> sources
|
||||||
|
val map = sources.mapNotNull { (_, src) ->
|
||||||
|
(src.patchBundle ?: return@mapNotNull null) to src
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
val metadata = try {
|
||||||
|
PatchBundle.Loader.metadata(map.keys)
|
||||||
|
} catch (error: Throwable) {
|
||||||
|
val uids = map.values.map { it.uid }
|
||||||
|
|
||||||
|
dispatchAction("Mark bundles as failed") { state ->
|
||||||
|
state.copy(sources = state.sources.mutate {
|
||||||
|
uids.forEach { uid ->
|
||||||
|
it[uid] = it[uid]?.copy(error = error) ?: return@forEach
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.e(tag, "Failed to load bundles", error)
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata.entries.associate { (bundle, patches) ->
|
||||||
|
val src = map[bundle]!!
|
||||||
|
src.uid to PatchBundleInfo.Global(
|
||||||
|
src.name,
|
||||||
|
bundle.manifestAttributes?.version,
|
||||||
|
src.uid,
|
||||||
|
patches
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun isVersionAllowed(packageName: String, version: String) =
|
suspend fun isVersionAllowed(packageName: String, version: String) =
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
if (!prefs.suggestedVersionSafeguard.get()) return@withContext true
|
if (!prefs.suggestedVersionSafeguard.get()) return@withContext true
|
||||||
@ -89,96 +206,211 @@ class PatchBundleRepository(
|
|||||||
|
|
||||||
private fun PatchBundleEntity.load(): PatchBundleSource {
|
private fun PatchBundleEntity.load(): PatchBundleSource {
|
||||||
val dir = directoryOf(uid)
|
val dir = directoryOf(uid)
|
||||||
|
val actualName =
|
||||||
|
name.ifEmpty { app.getString(if (uid == 0) R.string.patches_name_default else R.string.patches_name_fallback) }
|
||||||
|
|
||||||
return when (source) {
|
return when (source) {
|
||||||
is SourceInfo.Local -> LocalPatchBundle(name, uid, dir)
|
is SourceInfo.Local -> LocalPatchBundle(actualName, uid, null, dir)
|
||||||
is SourceInfo.API -> APIPatchBundle(name, uid, dir, SourceInfo.API.SENTINEL)
|
is SourceInfo.API -> APIPatchBundle(
|
||||||
is SourceInfo.Remote -> JsonPatchBundle(
|
actualName,
|
||||||
name,
|
|
||||||
uid,
|
uid,
|
||||||
|
versionHash,
|
||||||
|
null,
|
||||||
dir,
|
dir,
|
||||||
source.url.toString()
|
SourceInfo.API.SENTINEL,
|
||||||
|
autoUpdate,
|
||||||
|
)
|
||||||
|
|
||||||
|
is SourceInfo.Remote -> JsonPatchBundle(
|
||||||
|
actualName,
|
||||||
|
uid,
|
||||||
|
versionHash,
|
||||||
|
null,
|
||||||
|
dir,
|
||||||
|
source.url.toString(),
|
||||||
|
autoUpdate,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reload() = withContext(Dispatchers.Default) {
|
private suspend fun createEntity(name: String, source: Source, autoUpdate: Boolean = false) =
|
||||||
val entities = persistenceRepo.loadConfiguration().onEach {
|
PatchBundleEntity(
|
||||||
Log.d(tag, "Bundle: $it")
|
uid = generateUid(),
|
||||||
|
name = name,
|
||||||
|
versionHash = null,
|
||||||
|
source = source,
|
||||||
|
autoUpdate = autoUpdate
|
||||||
|
).also {
|
||||||
|
dao.upsert(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
_sources.value = entities.associate {
|
/**
|
||||||
it.uid to it.load()
|
* Updates a patch bundle in the database. Do not use this outside an action.
|
||||||
|
*/
|
||||||
|
private suspend fun updateDb(
|
||||||
|
uid: Int,
|
||||||
|
block: (PatchBundleProperties) -> PatchBundleProperties
|
||||||
|
) {
|
||||||
|
val previous = dao.getProps(uid)!!
|
||||||
|
val new = block(previous)
|
||||||
|
dao.upsert(
|
||||||
|
PatchBundleEntity(
|
||||||
|
uid = uid,
|
||||||
|
name = new.name,
|
||||||
|
versionHash = new.versionHash,
|
||||||
|
source = new.source,
|
||||||
|
autoUpdate = new.autoUpdate,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reset() = dispatchAction("Reset") { state ->
|
||||||
|
dao.reset()
|
||||||
|
state.sources.keys.forEach { directoryOf(it).deleteRecursively() }
|
||||||
|
doReload()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun remove(vararg bundles: PatchBundleSource) =
|
||||||
|
dispatchAction("Remove (${bundles.map { it.uid }.joinToString(",")})") { state ->
|
||||||
|
val sources = state.sources.toMutableMap()
|
||||||
|
val info = state.info.toMutableMap()
|
||||||
|
bundles.forEach {
|
||||||
|
if (it.isDefault) return@forEach
|
||||||
|
|
||||||
|
dao.remove(it.uid)
|
||||||
|
directoryOf(it.uid).deleteRecursively()
|
||||||
|
sources.remove(it.uid)
|
||||||
|
info.remove(it.uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
State(sources.toPersistentMap(), info.toPersistentMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createLocal(createStream: suspend () -> InputStream) = dispatchAction("Add bundle") {
|
||||||
|
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
|
||||||
|
try {
|
||||||
|
createStream().use { patches -> replace(patches) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
Log.e(tag, "Got exception while importing bundle", e)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteLocalFile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reset() = withContext(Dispatchers.Default) {
|
doReload()
|
||||||
persistenceRepo.reset()
|
|
||||||
_sources.value = emptyMap()
|
|
||||||
bundlesDir.apply {
|
|
||||||
deleteRecursively()
|
|
||||||
mkdirs()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reload()
|
suspend fun createRemote(url: String, autoUpdate: Boolean) =
|
||||||
|
dispatchAction("Add bundle ($url)") { state ->
|
||||||
|
val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle
|
||||||
|
update(src)
|
||||||
|
state.copy(sources = state.sources.put(src.uid, src))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun remove(bundle: PatchBundleSource) = withContext(Dispatchers.Default) {
|
suspend fun reloadApiBundles() = dispatchAction("Reload API bundles") {
|
||||||
persistenceRepo.delete(bundle.uid)
|
this@PatchBundleRepository.sources.first().filterIsInstance<APIPatchBundle>().forEach {
|
||||||
directoryOf(bundle.uid).deleteRecursively()
|
with(it) { deleteLocalFile() }
|
||||||
|
updateDb(it.uid) { it.copy(versionHash = null) }
|
||||||
_sources.update {
|
|
||||||
it.filterKeys { key ->
|
|
||||||
key != bundle.uid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addBundle(patchBundle: PatchBundleSource) =
|
doReload()
|
||||||
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }
|
|
||||||
|
|
||||||
suspend fun createLocal(patches: InputStream) = withContext(Dispatchers.Default) {
|
|
||||||
val uid = persistenceRepo.create("", SourceInfo.Local).uid
|
|
||||||
val bundle = LocalPatchBundle("", uid, directoryOf(uid))
|
|
||||||
|
|
||||||
bundle.replace(patches)
|
|
||||||
addBundle(bundle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createRemote(url: String, autoUpdate: Boolean) = withContext(Dispatchers.Default) {
|
suspend fun RemotePatchBundle.setAutoUpdate(value: Boolean) =
|
||||||
val entity = persistenceRepo.create("", SourceInfo.from(url), autoUpdate)
|
dispatchAction("Set auto update ($name, $value)") { state ->
|
||||||
addBundle(entity.load())
|
updateDb(uid) { it.copy(autoUpdate = value) }
|
||||||
|
val newSrc = (state.sources[uid] as? RemotePatchBundle)?.copy(autoUpdate = value)
|
||||||
|
?: return@dispatchAction state
|
||||||
|
|
||||||
|
state.copy(sources = state.sources.put(uid, newSrc))
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend inline fun <reified T> getBundlesByType() =
|
suspend fun update(vararg sources: RemotePatchBundle, showToast: Boolean = false) {
|
||||||
sources.first().filterIsInstance<T>()
|
val uids = sources.map { it.uid }.toSet()
|
||||||
|
store.dispatch(Update(showToast = showToast) { it.uid in uids })
|
||||||
suspend fun reloadApiBundles() {
|
|
||||||
getBundlesByType<APIPatchBundle>().forEach {
|
|
||||||
it.deleteLocalFiles()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reload()
|
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true))
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun redownloadRemoteBundles() =
|
/**
|
||||||
getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() }
|
* Updates all bundles that should be automatically updated.
|
||||||
|
*/
|
||||||
|
suspend fun updateCheck() = store.dispatch(Update { it.autoUpdate })
|
||||||
|
|
||||||
suspend fun updateCheck() =
|
private inner class Update(
|
||||||
uiSafe(app, R.string.source_download_fail, "Failed to update bundles") {
|
private val force: Boolean = false,
|
||||||
coroutineScope {
|
private val showToast: Boolean = false,
|
||||||
|
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
|
||||||
|
) : Action<State> {
|
||||||
|
private suspend fun toast(@StringRes id: Int, vararg args: Any?) =
|
||||||
|
withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) }
|
||||||
|
|
||||||
|
override fun toString() = if (force) "Redownload remote bundles" else "Update check"
|
||||||
|
|
||||||
|
override suspend fun ActionContext.execute(
|
||||||
|
current: State
|
||||||
|
) = coroutineScope {
|
||||||
if (!networkInfo.isSafe()) {
|
if (!networkInfo.isSafe()) {
|
||||||
Log.d(tag, "Skipping update check because the network is down or metered.")
|
Log.d(tag, "Skipping update check because the network is down or metered.")
|
||||||
return@coroutineScope
|
return@coroutineScope current
|
||||||
}
|
}
|
||||||
|
|
||||||
getBundlesByType<RemotePatchBundle>().forEach {
|
val updated = current.sources.values
|
||||||
launch {
|
.filterIsInstance<RemotePatchBundle>()
|
||||||
if (!it.getProps().autoUpdate) return@launch
|
.filter { predicate(it) }
|
||||||
Log.d(tag, "Updating patch bundle: ${it.getName()}")
|
.map {
|
||||||
it.update()
|
async {
|
||||||
|
Log.d(tag, "Updating patch bundle: ${it.name}")
|
||||||
|
|
||||||
|
val newVersion = with(it) {
|
||||||
|
if (force) downloadLatest() else update()
|
||||||
|
} ?: return@async null
|
||||||
|
|
||||||
|
it to newVersion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.filterNotNull()
|
||||||
|
.toMap()
|
||||||
|
if (updated.isEmpty()) {
|
||||||
|
if (showToast) toast(R.string.patches_update_unavailable)
|
||||||
|
return@coroutineScope current
|
||||||
|
}
|
||||||
|
|
||||||
|
updated.forEach { (src, newVersionHash) ->
|
||||||
|
val name = src.patchBundle?.manifestAttributes?.name ?: src.name
|
||||||
|
|
||||||
|
updateDb(src.uid) {
|
||||||
|
it.copy(versionHash = newVersionHash, name = name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showToast) toast(R.string.patches_update_success)
|
||||||
|
doReload()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun catch(exception: Exception) {
|
||||||
|
Log.e(tag, "Failed to update patches", exception)
|
||||||
|
toast(R.string.patches_download_fail, exception.simpleMessage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
val sources: PersistentMap<Int, PatchBundleSource> = persistentMapOf(),
|
||||||
|
val info: PersistentMap<Int, PatchBundleInfo.Global> = persistentMapOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val defaultSource = PatchBundleEntity(
|
||||||
|
uid = 0,
|
||||||
|
name = "",
|
||||||
|
versionHash = null,
|
||||||
|
source = Source.API,
|
||||||
|
autoUpdate = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,47 +1,73 @@
|
|||||||
package app.revanced.manager.patcher.patch
|
package app.revanced.manager.patcher.patch
|
||||||
|
|
||||||
import android.util.Log
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import app.revanced.manager.util.tag
|
import android.os.Parcelable
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.loadPatchesFromDex
|
||||||
import app.revanced.patcher.patch.PatchLoader
|
import kotlinx.parcelize.Parcelize
|
||||||
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
|
||||||
|
import kotlin.collections.filter
|
||||||
|
|
||||||
class PatchBundle(val patchesJar: File) {
|
@Parcelize
|
||||||
private val loader = object : Iterable<Patch<*>> {
|
data class PatchBundle(val patchesJar: String) : Parcelable {
|
||||||
private fun load(): Iterable<Patch<*>> {
|
|
||||||
patchesJar.setReadOnly()
|
|
||||||
return PatchLoader.Dex(setOf(patchesJar))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun iterator(): Iterator<Patch<*>> = load().iterator()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
Log.d(tag, "Loaded patch bundle: $patchesJar")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list containing the metadata of every patch inside this bundle.
|
|
||||||
*/
|
|
||||||
val patches = loader.map(::PatchInfo)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [java.util.jar.Manifest] of [patchesJar].
|
* The [java.util.jar.Manifest] of [patchesJar].
|
||||||
*/
|
*/
|
||||||
private val manifest = try {
|
@IgnoredOnParcel
|
||||||
|
private val manifest by lazy {
|
||||||
|
try {
|
||||||
JarFile(patchesJar).use { it.manifest }
|
JarFile(patchesJar).use { it.manifest }
|
||||||
} catch (_: IOException) {
|
} catch (_: IOException) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name)
|
@IgnoredOnParcel
|
||||||
|
val manifestAttributes by lazy {
|
||||||
|
if (manifest != null)
|
||||||
|
ManifestAttributes(
|
||||||
|
name = readManifestAttribute("name"),
|
||||||
|
version = readManifestAttribute("version"),
|
||||||
|
description = readManifestAttribute("description"),
|
||||||
|
source = readManifestAttribute("source"),
|
||||||
|
author = readManifestAttribute("author"),
|
||||||
|
contact = readManifestAttribute("contact"),
|
||||||
|
website = readManifestAttribute("website"),
|
||||||
|
license = readManifestAttribute("license")
|
||||||
|
) else
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
private fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name)
|
||||||
* Load all patches compatible with the specified package.
|
?.takeIf { it.isNotBlank() } // If empty, set it to null instead.
|
||||||
*/
|
|
||||||
fun patches(packageName: String) = loader.filter { patch ->
|
data class ManifestAttributes(
|
||||||
|
val name: String?,
|
||||||
|
val version: String?,
|
||||||
|
val description: String?,
|
||||||
|
val source: String?,
|
||||||
|
val author: String?,
|
||||||
|
val contact: String?,
|
||||||
|
val website: String?,
|
||||||
|
val license: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
object Loader {
|
||||||
|
private fun patches(bundles: Iterable<PatchBundle>) =
|
||||||
|
loadPatchesFromDex(
|
||||||
|
bundles.map { File(it.patchesJar) }.toSet()
|
||||||
|
).byPatchesFile.mapKeys { (file, _) ->
|
||||||
|
val absPath = file.absolutePath
|
||||||
|
bundles.single { absPath == it.patchesJar }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun metadata(bundles: Iterable<PatchBundle>) =
|
||||||
|
patches(bundles).mapValues { (_, patches) -> patches.map(::PatchInfo) }
|
||||||
|
|
||||||
|
fun patches(bundles: Iterable<PatchBundle>, packageName: String) =
|
||||||
|
patches(bundles).mapValues { (_, patches) ->
|
||||||
|
patches.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
|
||||||
@ -52,5 +78,7 @@ class PatchBundle(val patchesJar: File) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
package app.revanced.manager.patcher.patch
|
||||||
|
|
||||||
|
import app.revanced.manager.util.PatchSelection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A base class for storing [PatchBundle] metadata.
|
||||||
|
*/
|
||||||
|
sealed class PatchBundleInfo {
|
||||||
|
/**
|
||||||
|
* The name of the bundle.
|
||||||
|
*/
|
||||||
|
abstract val name: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version of the bundle.
|
||||||
|
*/
|
||||||
|
abstract val version: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique ID of the bundle.
|
||||||
|
*/
|
||||||
|
abstract val uid: Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The patch list.
|
||||||
|
*/
|
||||||
|
abstract val patches: List<PatchInfo>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about a bundle and all the patches it contains.
|
||||||
|
*
|
||||||
|
* @see [PatchBundleInfo]
|
||||||
|
*/
|
||||||
|
data class Global(
|
||||||
|
override val name: String,
|
||||||
|
override val version: String?,
|
||||||
|
override val uid: Int,
|
||||||
|
override val patches: List<PatchInfo>
|
||||||
|
) : PatchBundleInfo() {
|
||||||
|
/**
|
||||||
|
* Create a [PatchBundleInfo.Scoped] that only contains information about patches that are relevant for a specific [packageName].
|
||||||
|
*/
|
||||||
|
fun forPackage(packageName: String, version: String?): Scoped {
|
||||||
|
val relevantPatches = patches.filter { it.compatibleWith(packageName) }
|
||||||
|
val compatible = mutableListOf<PatchInfo>()
|
||||||
|
val incompatible = mutableListOf<PatchInfo>()
|
||||||
|
val universal = mutableListOf<PatchInfo>()
|
||||||
|
|
||||||
|
relevantPatches.forEach {
|
||||||
|
val targetList = when {
|
||||||
|
it.compatiblePackages == null -> universal
|
||||||
|
it.supports(
|
||||||
|
packageName,
|
||||||
|
version
|
||||||
|
) -> compatible
|
||||||
|
|
||||||
|
else -> incompatible
|
||||||
|
}
|
||||||
|
|
||||||
|
targetList.add(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scoped(
|
||||||
|
name,
|
||||||
|
this.version,
|
||||||
|
uid,
|
||||||
|
relevantPatches,
|
||||||
|
compatible,
|
||||||
|
incompatible,
|
||||||
|
universal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains information about a bundle that is relevant for a specific package name.
|
||||||
|
*
|
||||||
|
* @param compatible Patches that are compatible with the specified package name and version.
|
||||||
|
* @param incompatible Patches that are compatible with the specified package name but not version.
|
||||||
|
* @param universal Patches that are compatible with all packages.
|
||||||
|
* @see [PatchBundleInfo.Global.forPackage]
|
||||||
|
* @see [PatchBundleInfo]
|
||||||
|
*/
|
||||||
|
data class Scoped(
|
||||||
|
override val name: String,
|
||||||
|
override val version: String?,
|
||||||
|
override val uid: Int,
|
||||||
|
override val patches: List<PatchInfo>,
|
||||||
|
val compatible: List<PatchInfo>,
|
||||||
|
val incompatible: List<PatchInfo>,
|
||||||
|
val universal: List<PatchInfo>
|
||||||
|
) : PatchBundleInfo() {
|
||||||
|
fun patchSequence(allowIncompatible: Boolean) = if (allowIncompatible) {
|
||||||
|
patches.asSequence()
|
||||||
|
} else {
|
||||||
|
sequence {
|
||||||
|
yieldAll(compatible)
|
||||||
|
yieldAll(universal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object Extensions {
|
||||||
|
inline fun Iterable<Scoped>.toPatchSelection(
|
||||||
|
allowIncompatible: Boolean,
|
||||||
|
condition: (Int, PatchInfo) -> Boolean
|
||||||
|
): PatchSelection = this.associate { bundle ->
|
||||||
|
val patches =
|
||||||
|
bundle.patchSequence(allowIncompatible)
|
||||||
|
.mapNotNullTo(mutableSetOf()) { patch ->
|
||||||
|
patch.name.takeIf {
|
||||||
|
condition(
|
||||||
|
bundle.uid,
|
||||||
|
patch
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle.uid to patches
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Algorithm for determining whether all required options have been set.
|
||||||
|
*/
|
||||||
|
inline fun Iterable<Scoped>.requiredOptionsSet(
|
||||||
|
allowIncompatible: Boolean,
|
||||||
|
crossinline isSelected: (Scoped, PatchInfo) -> Boolean,
|
||||||
|
crossinline optionsForPatch: (Scoped, PatchInfo) -> Map<String, Any?>?
|
||||||
|
) = all bundle@{ bundle ->
|
||||||
|
bundle
|
||||||
|
.patchSequence(allowIncompatible)
|
||||||
|
.filter { isSelected(bundle, it) }
|
||||||
|
.all patch@{
|
||||||
|
if (it.options.isNullOrEmpty()) return@patch true
|
||||||
|
val opts by lazy { optionsForPatch(bundle, it).orEmpty() }
|
||||||
|
|
||||||
|
it.options.all option@{ option ->
|
||||||
|
if (!option.required || option.default != null) return@option true
|
||||||
|
|
||||||
|
option.key in opts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ package app.revanced.manager.patcher.runtime
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.revanced.manager.patcher.Session
|
import app.revanced.manager.patcher.Session
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||||
import app.revanced.manager.ui.model.State
|
import app.revanced.manager.ui.model.State
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
@ -23,14 +24,17 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
|||||||
onPatchCompleted: suspend () -> Unit,
|
onPatchCompleted: suspend () -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
onProgress: ProgressEventHandler,
|
||||||
) {
|
) {
|
||||||
val bundles = bundles()
|
|
||||||
|
|
||||||
val selectedBundles = selectedPatches.keys
|
val selectedBundles = selectedPatches.keys
|
||||||
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
|
val bundles = bundles()
|
||||||
.mapValues { (_, bundle) -> bundle.patches(packageName) }
|
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||||
|
|
||||||
|
val allPatches =
|
||||||
|
PatchBundle.Loader.patches(bundles.values, packageName)
|
||||||
|
.mapKeys { (b, _) -> uids[b]!! }
|
||||||
|
.filterKeys { it in selectedBundles }
|
||||||
|
|
||||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||||
allPatches[bundle]?.filter { selected.contains(it.name) }
|
allPatches[bundle]?.filter { it.name in selected }
|
||||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,8 +142,6 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val bundles = bundles()
|
|
||||||
|
|
||||||
val parameters = Parameters(
|
val parameters = Parameters(
|
||||||
aaptPath = aaptPath,
|
aaptPath = aaptPath,
|
||||||
frameworkDir = frameworkPath,
|
frameworkDir = frameworkPath,
|
||||||
@ -151,13 +149,11 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
packageName = packageName,
|
packageName = packageName,
|
||||||
inputFile = inputFile,
|
inputFile = inputFile,
|
||||||
outputFile = outputFile,
|
outputFile = outputFile,
|
||||||
configurations = selectedPatches.map { (id, patches) ->
|
configurations = bundles().map { (uid, bundle) ->
|
||||||
val bundle = bundles[id]!!
|
|
||||||
|
|
||||||
PatchConfiguration(
|
PatchConfiguration(
|
||||||
bundle.patchesJar.absolutePath,
|
bundle,
|
||||||
patches,
|
selectedPatches[uid].orEmpty(),
|
||||||
options[id].orEmpty()
|
options[uid].orEmpty()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.revanced.manager.patcher.runtime.process
|
package app.revanced.manager.patcher.runtime.process
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.parcelize.RawValue
|
import kotlinx.parcelize.RawValue
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ data class Parameters(
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class PatchConfiguration(
|
data class PatchConfiguration(
|
||||||
val bundlePath: String,
|
val bundle: PatchBundle,
|
||||||
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
|
@ -56,11 +56,10 @@ 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 allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName)
|
||||||
val patchList = parameters.configurations.flatMap { config ->
|
val patchList = parameters.configurations.flatMap { config ->
|
||||||
val bundle = PatchBundle(File(config.bundlePath))
|
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
||||||
|
.filter { it.name in config.patches }
|
||||||
val 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) ->
|
||||||
|
@ -14,9 +14,9 @@ import android.os.Parcelable
|
|||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.work.ForegroundInfo
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import app.revanced.manager.MainActivity
|
||||||
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.data.room.apps.installed.InstallType
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
@ -88,22 +88,25 @@ class PatcherWorker(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private fun createNotification(): Notification {
|
private fun createNotification(): Notification {
|
||||||
val notificationIntent = Intent(applicationContext, PatcherWorker::class.java)
|
val notificationIntent = Intent(applicationContext, MainActivity::class.java).apply {
|
||||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
}
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
|
applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
"revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_HIGH
|
"revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_LOW
|
||||||
)
|
)
|
||||||
val notificationManager =
|
val notificationManager =
|
||||||
ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
|
applicationContext.getSystemService(NotificationManager::class.java)
|
||||||
notificationManager!!.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
return Notification.Builder(applicationContext, channel.id)
|
return Notification.Builder(applicationContext, channel.id)
|
||||||
.setContentTitle(applicationContext.getText(R.string.app_name))
|
.setContentTitle(applicationContext.getText(R.string.patcher_notification_title))
|
||||||
.setContentText(applicationContext.getText(R.string.patcher_notification_message))
|
.setContentText(applicationContext.getText(R.string.patcher_notification_text))
|
||||||
.setLargeIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
|
|
||||||
.setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
|
.setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification))
|
||||||
.setContentIntent(pendingIntent).build()
|
.setContentIntent(pendingIntent)
|
||||||
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
@ -158,6 +161,8 @@ class PatcherWorker(
|
|||||||
data,
|
data,
|
||||||
args.packageName,
|
args.packageName,
|
||||||
args.input.version,
|
args.input.version,
|
||||||
|
prefs.suggestedVersionSafeguard.get(),
|
||||||
|
!prefs.disablePatchVersionCompatCheck.get(),
|
||||||
onDownload = args.onDownloadProgress
|
onDownload = args.onDownloadProgress
|
||||||
).also {
|
).also {
|
||||||
args.setInputFile(it)
|
args.setInputFile(it)
|
||||||
|
@ -30,7 +30,7 @@ fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) {
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
BundleTopBar(
|
BundleTopBar(
|
||||||
title = stringResource(R.string.bundle_error),
|
title = stringResource(R.string.patches_error),
|
||||||
onBackClick = onDismiss,
|
onBackClick = onDismiss,
|
||||||
backIcon = {
|
backIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.revanced.manager.ui.component
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@ -9,6 +10,7 @@ import androidx.compose.ui.platform.LocalView
|
|||||||
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.compose.ui.window.DialogWindowProvider
|
import androidx.compose.ui.window.DialogWindowProvider
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
|
||||||
private val properties = DialogProperties(
|
private val properties = DialogProperties(
|
||||||
usePlatformDefaultWidth = false,
|
usePlatformDefaultWidth = false,
|
||||||
@ -22,11 +24,17 @@ fun FullscreenDialog(onDismissRequest: () -> Unit, content: @Composable () -> Un
|
|||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
properties = properties
|
properties = properties
|
||||||
) {
|
) {
|
||||||
val window = (LocalView.current.parent as DialogWindowProvider).window
|
val view = LocalView.current
|
||||||
LaunchedEffect(Unit) {
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
LaunchedEffect(isDarkTheme) {
|
||||||
|
val window = (view.parent as DialogWindowProvider).window
|
||||||
window.statusBarColor = Color.Transparent.toArgb()
|
window.statusBarColor = Color.Transparent.toArgb()
|
||||||
window.navigationBarColor = Color.Transparent.toArgb()
|
window.navigationBarColor = Color.Transparent.toArgb()
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
|
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
|
||||||
|
|
||||||
|
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||||
|
insetsController.isAppearanceLightStatusBars = !isDarkTheme
|
||||||
|
insetsController.isAppearanceLightNavigationBars = !isDarkTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
content()
|
content()
|
||||||
|
@ -1,181 +0,0 @@
|
|||||||
package app.revanced.manager.ui.component.bundle
|
|
||||||
|
|
||||||
import android.webkit.URLUtil
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
|
||||||
import androidx.compose.material.icons.outlined.Extension
|
|
||||||
import androidx.compose.material.icons.outlined.Inventory2
|
|
||||||
import androidx.compose.material.icons.outlined.Sell
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import app.revanced.manager.R
|
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
|
||||||
import app.revanced.manager.ui.component.TextInputDialog
|
|
||||||
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BaseBundleDialog(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
isDefault: Boolean,
|
|
||||||
name: String?,
|
|
||||||
remoteUrl: String?,
|
|
||||||
onRemoteUrlChange: ((String) -> Unit)? = null,
|
|
||||||
patchCount: Int,
|
|
||||||
version: String?,
|
|
||||||
autoUpdate: Boolean,
|
|
||||||
onAutoUpdateChange: (Boolean) -> Unit,
|
|
||||||
onPatchesClick: () -> Unit,
|
|
||||||
extraFields: @Composable ColumnScope.() -> Unit = {}
|
|
||||||
) {
|
|
||||||
ColumnWithScrollbar(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.then(modifier),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Inventory2,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(32.dp)
|
|
||||||
)
|
|
||||||
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(
|
|
||||||
headlineText = stringResource(R.string.bundle_auto_update),
|
|
||||||
supportingText = stringResource(R.string.bundle_auto_update_description),
|
|
||||||
trailingContent = {
|
|
||||||
HapticSwitch(
|
|
||||||
checked = autoUpdate,
|
|
||||||
onCheckedChange = onAutoUpdateChange
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
onAutoUpdateChange(!autoUpdate)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteUrl?.takeUnless { isDefault }?.let { url ->
|
|
||||||
var showUrlInputDialog by rememberSaveable {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
if (showUrlInputDialog) {
|
|
||||||
TextInputDialog(
|
|
||||||
initial = url,
|
|
||||||
title = stringResource(R.string.bundle_input_source_url),
|
|
||||||
onDismissRequest = { showUrlInputDialog = false },
|
|
||||||
onConfirm = {
|
|
||||||
showUrlInputDialog = false
|
|
||||||
onRemoteUrlChange?.invoke(it)
|
|
||||||
},
|
|
||||||
validator = {
|
|
||||||
if (it.isEmpty()) return@TextInputDialog false
|
|
||||||
|
|
||||||
URLUtil.isValidUrl(it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
BundleListItem(
|
|
||||||
modifier = Modifier.clickable(
|
|
||||||
enabled = onRemoteUrlChange != null,
|
|
||||||
onClick = {
|
|
||||||
showUrlInputDialog = true
|
|
||||||
}
|
|
||||||
),
|
|
||||||
headlineText = stringResource(R.string.bundle_input_source_url),
|
|
||||||
supportingText = url.ifEmpty {
|
|
||||||
stringResource(R.string.field_not_set)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val patchesClickable = patchCount > 0
|
|
||||||
BundleListItem(
|
|
||||||
headlineText = stringResource(R.string.patches),
|
|
||||||
supportingText = stringResource(R.string.bundle_view_patches),
|
|
||||||
modifier = Modifier.clickable(
|
|
||||||
enabled = patchesClickable,
|
|
||||||
onClick = onPatchesClick
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (patchesClickable) {
|
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Outlined.ArrowRight,
|
|
||||||
stringResource(R.string.patches)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extraFields()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,68 +1,99 @@
|
|||||||
package app.revanced.manager.ui.component.bundle
|
package app.revanced.manager.ui.component.bundle
|
||||||
|
|
||||||
|
import android.webkit.URLUtil.isValidUrl
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
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.automirrored.outlined.ArrowRight
|
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.Send
|
||||||
|
import androidx.compose.material.icons.outlined.Commit
|
||||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||||
|
import androidx.compose.material.icons.outlined.Description
|
||||||
|
import androidx.compose.material.icons.outlined.Gavel
|
||||||
|
import androidx.compose.material.icons.outlined.Language
|
||||||
|
import androidx.compose.material.icons.outlined.Person
|
||||||
|
import androidx.compose.material.icons.outlined.Sell
|
||||||
import androidx.compose.material.icons.outlined.Update
|
import androidx.compose.material.icons.outlined.Update
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.R.string.auto_update
|
||||||
|
import app.revanced.manager.R.string.auto_update_description
|
||||||
|
import app.revanced.manager.R.string.field_not_set
|
||||||
|
import app.revanced.manager.R.string.patches
|
||||||
|
import app.revanced.manager.R.string.patches_url
|
||||||
|
import app.revanced.manager.R.string.view_patches
|
||||||
import app.revanced.manager.data.platform.NetworkInfo
|
import app.revanced.manager.data.platform.NetworkInfo
|
||||||
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
||||||
import app.revanced.manager.ui.component.FullscreenDialog
|
import app.revanced.manager.ui.component.FullscreenDialog
|
||||||
|
import app.revanced.manager.ui.component.TextInputDialog
|
||||||
|
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BundleInformationDialog(
|
fun BundleInformationDialog(
|
||||||
|
src: PatchBundleSource,
|
||||||
|
patchCount: Int,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDeleteRequest: () -> Unit,
|
onDeleteRequest: () -> Unit,
|
||||||
bundle: PatchBundleSource,
|
|
||||||
onUpdate: () -> Unit,
|
onUpdate: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val bundleRepo = koinInject<PatchBundleRepository>()
|
||||||
val networkInfo = koinInject<NetworkInfo>()
|
val networkInfo = koinInject<NetworkInfo>()
|
||||||
val hasNetwork = remember { networkInfo.isConnected() }
|
val hasNetwork = remember { networkInfo.isConnected() }
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
var viewCurrentBundlePatches by remember { mutableStateOf(false) }
|
var viewCurrentBundlePatches by remember { mutableStateOf(false) }
|
||||||
val isLocal = bundle is LocalPatchBundle
|
val isLocal = src is LocalPatchBundle
|
||||||
val state by bundle.state.collectAsStateWithLifecycle()
|
val bundleManifestAttributes = src.patchBundle?.manifestAttributes
|
||||||
val props by remember(bundle) {
|
val (autoUpdate, endpoint) = src.asRemoteOrNull?.let { it.autoUpdate to it.endpoint } ?: (null to null)
|
||||||
bundle.propsFlow()
|
|
||||||
}.collectAsStateWithLifecycle(null)
|
fun onAutoUpdateChange(new: Boolean) = composableScope.launch {
|
||||||
val patchCount by bundle.patchCountFlow.collectAsStateWithLifecycle(0)
|
with(bundleRepo) {
|
||||||
val version by bundle.versionFlow.collectAsStateWithLifecycle(null)
|
src.asRemoteOrNull?.setAutoUpdate(new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (viewCurrentBundlePatches) {
|
if (viewCurrentBundlePatches) {
|
||||||
BundlePatchesDialog(
|
BundlePatchesDialog(
|
||||||
|
src = src,
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
viewCurrentBundlePatches = false
|
viewCurrentBundlePatches = false
|
||||||
},
|
}
|
||||||
bundle = bundle,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
FullscreenDialog(
|
FullscreenDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
) {
|
) {
|
||||||
val bundleName by bundle.nameState
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
BundleTopBar(
|
BundleTopBar(
|
||||||
title = stringResource(R.string.patch_bundle_field),
|
title = src.name,
|
||||||
onBackClick = onDismissRequest,
|
onBackClick = onDismissRequest,
|
||||||
backIcon = {
|
backIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
@ -71,7 +102,7 @@ fun BundleInformationDialog(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (!bundle.isDefault) {
|
if (!src.isDefault) {
|
||||||
IconButton(onClick = onDeleteRequest) {
|
IconButton(onClick = onDeleteRequest) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.DeleteOutline,
|
Icons.Outlined.DeleteOutline,
|
||||||
@ -91,24 +122,112 @@ fun BundleInformationDialog(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
BaseBundleDialog(
|
ColumnWithScrollbar(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier
|
||||||
isDefault = bundle.isDefault,
|
.fillMaxWidth()
|
||||||
name = bundleName,
|
.padding(paddingValues),
|
||||||
remoteUrl = bundle.asRemoteOrNull?.endpoint,
|
) {
|
||||||
patchCount = patchCount,
|
Column(
|
||||||
version = version,
|
modifier = Modifier.padding(16.dp),
|
||||||
autoUpdate = props?.autoUpdate == true,
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
onAutoUpdateChange = {
|
) {
|
||||||
composableScope.launch {
|
Tag(Icons.Outlined.Sell, src.name)
|
||||||
bundle.asRemoteOrNull?.setAutoUpdate(it)
|
bundleManifestAttributes?.description?.let {
|
||||||
|
Tag(Icons.Outlined.Description, it)
|
||||||
}
|
}
|
||||||
|
bundleManifestAttributes?.source?.let {
|
||||||
|
Tag(Icons.Outlined.Commit, it)
|
||||||
|
}
|
||||||
|
bundleManifestAttributes?.author?.let {
|
||||||
|
Tag(Icons.Outlined.Person, it)
|
||||||
|
}
|
||||||
|
bundleManifestAttributes?.contact?.let {
|
||||||
|
Tag(Icons.AutoMirrored.Outlined.Send, it)
|
||||||
|
}
|
||||||
|
bundleManifestAttributes?.website?.let {
|
||||||
|
Tag(Icons.Outlined.Language, it, isUrl = true)
|
||||||
|
}
|
||||||
|
bundleManifestAttributes?.license?.let {
|
||||||
|
Tag(Icons.Outlined.Gavel, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
if (autoUpdate != null) {
|
||||||
|
BundleListItem(
|
||||||
|
headlineText = stringResource(auto_update),
|
||||||
|
supportingText = stringResource(auto_update_description),
|
||||||
|
trailingContent = {
|
||||||
|
HapticSwitch(
|
||||||
|
checked = autoUpdate,
|
||||||
|
onCheckedChange = ::onAutoUpdateChange
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onPatchesClick = {
|
modifier = Modifier.clickable {
|
||||||
|
onAutoUpdateChange(!autoUpdate)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint?.takeUnless { src.isDefault }?.let { url ->
|
||||||
|
var showUrlInputDialog by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
if (showUrlInputDialog) {
|
||||||
|
TextInputDialog(
|
||||||
|
initial = url,
|
||||||
|
title = stringResource(patches_url),
|
||||||
|
onDismissRequest = { showUrlInputDialog = false },
|
||||||
|
onConfirm = {
|
||||||
|
showUrlInputDialog = false
|
||||||
|
TODO("Not implemented.")
|
||||||
|
},
|
||||||
|
validator = {
|
||||||
|
if (it.isEmpty()) return@TextInputDialog false
|
||||||
|
|
||||||
|
isValidUrl(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BundleListItem(
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
enabled = false,
|
||||||
|
onClick = {
|
||||||
|
showUrlInputDialog = true
|
||||||
|
}
|
||||||
|
),
|
||||||
|
headlineText = stringResource(patches_url),
|
||||||
|
supportingText = url.ifEmpty {
|
||||||
|
stringResource(field_not_set)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val patchesClickable = patchCount > 0
|
||||||
|
BundleListItem(
|
||||||
|
headlineText = stringResource(patches),
|
||||||
|
supportingText = stringResource(view_patches),
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
enabled = patchesClickable,
|
||||||
|
onClick = {
|
||||||
viewCurrentBundlePatches = true
|
viewCurrentBundlePatches = true
|
||||||
},
|
}
|
||||||
extraFields = {
|
)
|
||||||
(state as? PatchBundleSource.State.Failed)?.throwable?.let {
|
) {
|
||||||
|
if (patchesClickable) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Outlined.ArrowRight,
|
||||||
|
stringResource(patches)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
src.error?.let {
|
||||||
var showDialog by rememberSaveable {
|
var showDialog by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
@ -118,8 +237,8 @@ fun BundleInformationDialog(
|
|||||||
)
|
)
|
||||||
|
|
||||||
BundleListItem(
|
BundleListItem(
|
||||||
headlineText = stringResource(R.string.bundle_error),
|
headlineText = stringResource(R.string.patches_error),
|
||||||
supportingText = stringResource(R.string.bundle_error_description),
|
supportingText = stringResource(R.string.patches_error_description),
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Outlined.ArrowRight,
|
Icons.AutoMirrored.Outlined.ArrowRight,
|
||||||
@ -129,16 +248,49 @@ fun BundleInformationDialog(
|
|||||||
modifier = Modifier.clickable { showDialog = true }
|
modifier = Modifier.clickable { showDialog = true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (src.state is PatchBundleSource.State.Missing && !isLocal) {
|
||||||
if (state is PatchBundleSource.State.Missing && !isLocal) {
|
|
||||||
BundleListItem(
|
BundleListItem(
|
||||||
headlineText = stringResource(R.string.bundle_error),
|
headlineText = stringResource(R.string.patches_error),
|
||||||
supportingText = stringResource(R.string.bundle_not_downloaded),
|
supportingText = stringResource(R.string.patches_not_downloaded),
|
||||||
modifier = Modifier.clickable(onClick = onUpdate)
|
modifier = Modifier.clickable(onClick = onUpdate)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Tag(
|
||||||
|
icon: ImageVector,
|
||||||
|
text: String,
|
||||||
|
isUrl: Boolean = false
|
||||||
|
) {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = if (isUrl) {
|
||||||
|
Modifier
|
||||||
|
.clickable {
|
||||||
|
try {
|
||||||
|
uriHandler.openUri(text)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
Modifier,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (isUrl) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
@ -24,38 +24,32 @@ 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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
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.domain.bundles.PatchBundleSource.Extensions.nameState
|
|
||||||
import app.revanced.manager.ui.component.ConfirmDialog
|
import app.revanced.manager.ui.component.ConfirmDialog
|
||||||
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BundleItem(
|
fun BundleItem(
|
||||||
bundle: PatchBundleSource,
|
src: PatchBundleSource,
|
||||||
onDelete: () -> Unit,
|
patchCount: Int,
|
||||||
onUpdate: () -> Unit,
|
|
||||||
selectable: Boolean,
|
selectable: Boolean,
|
||||||
onSelect: () -> Unit,
|
|
||||||
isBundleSelected: Boolean,
|
isBundleSelected: Boolean,
|
||||||
toggleSelection: (Boolean) -> Unit,
|
toggleSelection: (Boolean) -> Unit,
|
||||||
|
onSelect: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
onUpdate: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) }
|
var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) }
|
||||||
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
val state by bundle.state.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
val version by bundle.versionFlow.collectAsStateWithLifecycle(null)
|
|
||||||
val patchCount by bundle.patchCountFlow.collectAsStateWithLifecycle(0)
|
|
||||||
val name by bundle.nameState
|
|
||||||
|
|
||||||
if (viewBundleDialogPage) {
|
if (viewBundleDialogPage) {
|
||||||
BundleInformationDialog(
|
BundleInformationDialog(
|
||||||
|
src = src,
|
||||||
|
patchCount = patchCount,
|
||||||
onDismissRequest = { viewBundleDialogPage = false },
|
onDismissRequest = { viewBundleDialogPage = false },
|
||||||
onDeleteRequest = { showDeleteConfirmationDialog = true },
|
onDeleteRequest = { showDeleteConfirmationDialog = true },
|
||||||
bundle = bundle,
|
|
||||||
onUpdate = onUpdate,
|
onUpdate = onUpdate,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -67,8 +61,8 @@ fun BundleItem(
|
|||||||
onDelete()
|
onDelete()
|
||||||
viewBundleDialogPage = false
|
viewBundleDialogPage = false
|
||||||
},
|
},
|
||||||
title = stringResource(R.string.bundle_delete_single_dialog_title),
|
title = stringResource(R.string.delete),
|
||||||
description = stringResource(R.string.bundle_delete_single_dialog_description, name),
|
description = stringResource(R.string.patches_delete_single_dialog_description, src.name),
|
||||||
icon = Icons.Outlined.Delete
|
icon = Icons.Outlined.Delete
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -90,19 +84,19 @@ fun BundleItem(
|
|||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
|
|
||||||
headlineContent = { Text(name) },
|
headlineContent = { Text(src.name) },
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
if (state is PatchBundleSource.State.Loaded) {
|
if (src.state is PatchBundleSource.State.Available) {
|
||||||
Text(pluralStringResource(R.plurals.patch_count, patchCount, patchCount))
|
Text(pluralStringResource(R.plurals.patch_count, patchCount, patchCount))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Row {
|
Row {
|
||||||
val icon = remember(state) {
|
val icon = remember(src.state) {
|
||||||
when (state) {
|
when (src.state) {
|
||||||
is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.bundle_error
|
is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.patches_error
|
||||||
is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.bundle_missing
|
is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.patches_missing
|
||||||
is PatchBundleSource.State.Loaded -> null
|
is PatchBundleSource.State.Available -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +109,7 @@ fun BundleItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
version?.let { Text(text = it) }
|
src.version?.let { Text(text = it) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -12,6 +12,7 @@ import androidx.compose.material3.*
|
|||||||
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.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@ -25,20 +26,26 @@ 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.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.ui.component.ArrowButton
|
import app.revanced.manager.ui.component.ArrowButton
|
||||||
import app.revanced.manager.ui.component.FullscreenDialog
|
import app.revanced.manager.ui.component.FullscreenDialog
|
||||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BundlePatchesDialog(
|
fun BundlePatchesDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
bundle: PatchBundleSource,
|
src: PatchBundleSource,
|
||||||
) {
|
) {
|
||||||
var showAllVersions by rememberSaveable { mutableStateOf(false) }
|
var showAllVersions by rememberSaveable { mutableStateOf(false) }
|
||||||
var showOptions by rememberSaveable { mutableStateOf(false) }
|
var showOptions by rememberSaveable { mutableStateOf(false) }
|
||||||
val state by bundle.state.collectAsStateWithLifecycle()
|
val patchBundleRepository: PatchBundleRepository = koinInject()
|
||||||
|
val patches by remember(src.uid) {
|
||||||
|
patchBundleRepository.bundleInfoFlow.mapNotNull { it[src.uid]?.patches }
|
||||||
|
}.collectAsStateWithLifecycle(emptyList())
|
||||||
|
|
||||||
FullscreenDialog(
|
FullscreenDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
@ -46,7 +53,7 @@ fun BundlePatchesDialog(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
BundleTopBar(
|
BundleTopBar(
|
||||||
title = stringResource(R.string.bundle_patches),
|
title = stringResource(R.string.patches),
|
||||||
onBackClick = onDismissRequest,
|
onBackClick = onDismissRequest,
|
||||||
backIcon = {
|
backIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
@ -64,8 +71,7 @@ fun BundlePatchesDialog(
|
|||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
contentPadding = PaddingValues(16.dp)
|
contentPadding = PaddingValues(16.dp)
|
||||||
) {
|
) {
|
||||||
state.patchBundleOrNull()?.let { bundle ->
|
items(patches) { patch ->
|
||||||
items(bundle.patches) { patch ->
|
|
||||||
PatchItem(
|
PatchItem(
|
||||||
patch,
|
patch,
|
||||||
showAllVersions,
|
showAllVersions,
|
||||||
@ -78,7 +84,6 @@ fun BundlePatchesDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -133,10 +138,10 @@ fun PatchItem(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
PatchInfoChip(
|
PatchInfoChip(
|
||||||
text = "$PACKAGE_ICON ${stringResource(R.string.bundle_view_patches_any_package)}"
|
text = "$PACKAGE_ICON ${stringResource(R.string.patches_view_any_package)}"
|
||||||
)
|
)
|
||||||
PatchInfoChip(
|
PatchInfoChip(
|
||||||
text = "$VERSION_ICON ${stringResource(R.string.bundle_view_patches_any_version)}"
|
text = "$VERSION_ICON ${stringResource(R.string.patches_view_any_version)}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -12,26 +12,23 @@ import androidx.compose.material3.ModalBottomSheet
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSource?) -> Unit) {
|
fun BundleSelector(sources: List<PatchBundleSource>, onFinish: (PatchBundleSource?) -> Unit) {
|
||||||
LaunchedEffect(bundles) {
|
LaunchedEffect(sources) {
|
||||||
if (bundles.size == 1) {
|
if (sources.size == 1) {
|
||||||
onFinish(bundles[0])
|
onFinish(sources[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bundles.size < 2) {
|
if (sources.size < 2) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,15 +47,12 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Select bundle",
|
text = stringResource(R.string.select),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
bundles.forEach {
|
sources.forEach {
|
||||||
val name by it.nameState
|
|
||||||
val version by it.versionFlow.collectAsStateWithLifecycle(null)
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
@ -70,7 +64,7 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"$name $version",
|
"${it.name} ${it.version}",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
@ -23,10 +23,14 @@ 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.HapticCheckbox
|
||||||
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
||||||
import app.revanced.manager.ui.model.BundleType
|
|
||||||
import app.revanced.manager.util.BIN_MIMETYPE
|
import app.revanced.manager.util.BIN_MIMETYPE
|
||||||
import app.revanced.manager.util.transparentListItemColors
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
|
|
||||||
|
private enum class BundleType {
|
||||||
|
Local,
|
||||||
|
Remote
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ImportPatchBundleDialog(
|
fun ImportPatchBundleDialog(
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
@ -37,7 +41,7 @@ fun ImportPatchBundleDialog(
|
|||||||
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 remoteUrl by rememberSaveable { mutableStateOf("") }
|
var remoteUrl by rememberSaveable { mutableStateOf("") }
|
||||||
var autoUpdate by rememberSaveable { mutableStateOf(false) }
|
var autoUpdate by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
val patchActivityLauncher =
|
val patchActivityLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||||
@ -77,7 +81,7 @@ fun ImportPatchBundleDialog(
|
|||||||
AlertDialogExtended(
|
AlertDialogExtended(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = {
|
title = {
|
||||||
Text(stringResource(if (currentStep == 0) R.string.select else R.string.add_patch_bundle))
|
Text(stringResource(if (currentStep == 0) R.string.select else R.string.add_patches))
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
steps[currentStep]()
|
steps[currentStep]()
|
||||||
@ -117,7 +121,7 @@ fun ImportPatchBundleDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectBundleTypeStep(
|
private fun SelectBundleTypeStep(
|
||||||
bundleType: BundleType,
|
bundleType: BundleType,
|
||||||
onBundleTypeSelected: (BundleType) -> Unit
|
onBundleTypeSelected: (BundleType) -> Unit
|
||||||
) {
|
) {
|
||||||
@ -126,7 +130,7 @@ fun SelectBundleTypeStep(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(horizontal = 24.dp),
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
text = stringResource(R.string.select_bundle_type_dialog_description)
|
text = stringResource(R.string.select_patches_type_dialog_description)
|
||||||
)
|
)
|
||||||
Column {
|
Column {
|
||||||
ListItem(
|
ListItem(
|
||||||
@ -136,7 +140,7 @@ fun SelectBundleTypeStep(
|
|||||||
),
|
),
|
||||||
headlineContent = { Text(stringResource(R.string.enter_url)) },
|
headlineContent = { Text(stringResource(R.string.enter_url)) },
|
||||||
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_patches_description)) },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
HapticRadioButton(
|
HapticRadioButton(
|
||||||
selected = bundleType == BundleType.Remote,
|
selected = bundleType == BundleType.Remote,
|
||||||
@ -152,7 +156,7 @@ fun SelectBundleTypeStep(
|
|||||||
onClick = { onBundleTypeSelected(BundleType.Local) }
|
onClick = { onBundleTypeSelected(BundleType.Local) }
|
||||||
),
|
),
|
||||||
headlineContent = { Text(stringResource(R.string.select_from_storage)) },
|
headlineContent = { Text(stringResource(R.string.select_from_storage)) },
|
||||||
supportingContent = { Text(stringResource(R.string.local_bundle_description)) },
|
supportingContent = { Text(stringResource(R.string.local_patches_description)) },
|
||||||
overlineContent = { },
|
overlineContent = { },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
HapticRadioButton(
|
HapticRadioButton(
|
||||||
@ -168,7 +172,7 @@ fun SelectBundleTypeStep(
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ImportBundleStep(
|
private fun ImportBundleStep(
|
||||||
bundleType: BundleType,
|
bundleType: BundleType,
|
||||||
patchBundle: Uri?,
|
patchBundle: Uri?,
|
||||||
remoteUrl: String,
|
remoteUrl: String,
|
||||||
@ -185,7 +189,7 @@ fun ImportBundleStep(
|
|||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(stringResource(R.string.patch_bundle_field))
|
Text(stringResource(R.string.patches))
|
||||||
},
|
},
|
||||||
supportingContent = { Text(stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set)) },
|
supportingContent = { Text(stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set)) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
@ -206,11 +210,11 @@ fun ImportBundleStep(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = remoteUrl,
|
value = remoteUrl,
|
||||||
onValueChange = onRemoteUrlChange,
|
onValueChange = onRemoteUrlChange,
|
||||||
label = { Text(stringResource(R.string.bundle_url)) }
|
label = { Text(stringResource(R.string.patches_url)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = 8.dp)
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 5.dp)
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable(
|
modifier = Modifier.clickable(
|
||||||
|
@ -9,6 +9,7 @@ import androidx.compose.material3.SwitchDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HapticSwitch(
|
fun HapticSwitch(
|
||||||
@ -20,16 +21,19 @@ fun HapticSwitch(
|
|||||||
colors: SwitchColors = SwitchDefaults.colors(),
|
colors: SwitchColors = SwitchDefaults.colors(),
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
) {
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
Switch(
|
Switch(
|
||||||
checked = checked,
|
checked = checked,
|
||||||
onCheckedChange = { newChecked ->
|
onCheckedChange = { newChecked ->
|
||||||
val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
||||||
when {
|
val hapticFeedbackType = when {
|
||||||
newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON
|
newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON
|
||||||
newChecked -> HapticFeedbackConstants.VIRTUAL_KEY
|
newChecked -> HapticFeedbackConstants.VIRTUAL_KEY
|
||||||
!newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF
|
!newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF
|
||||||
!newChecked -> HapticFeedbackConstants.CLOCK_TICK
|
!newChecked -> HapticFeedbackConstants.CLOCK_TICK
|
||||||
|
else -> {HapticFeedbackConstants.VIRTUAL_KEY}
|
||||||
}
|
}
|
||||||
|
view.performHapticFeedback(hapticFeedbackType)
|
||||||
onCheckedChange(newChecked)
|
onCheckedChange(newChecked)
|
||||||
},
|
},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
@ -74,13 +73,11 @@ import app.revanced.manager.util.saver.snapshotStateListSaver
|
|||||||
import app.revanced.manager.util.saver.snapshotStateSetSaver
|
import app.revanced.manager.util.saver.snapshotStateSetSaver
|
||||||
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 kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import sh.calvin.reorderable.ReorderableItem
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
import sh.calvin.reorderable.rememberReorderableLazyColumnState
|
|
||||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
@ -91,15 +88,28 @@ private class OptionEditorScope<T : Any>(
|
|||||||
val option: Option<T>,
|
val option: Option<T>,
|
||||||
val openDialog: () -> Unit,
|
val openDialog: () -> Unit,
|
||||||
val dismissDialog: () -> Unit,
|
val dismissDialog: () -> Unit,
|
||||||
|
val selectionWarningEnabled: Boolean,
|
||||||
|
val showSelectionWarning: () -> Unit,
|
||||||
val value: T?,
|
val value: T?,
|
||||||
val setValue: (T?) -> Unit,
|
val setValue: (T?) -> Unit
|
||||||
) {
|
) {
|
||||||
fun submitDialog(value: T?) {
|
fun submitDialog(value: T?) {
|
||||||
setValue(value)
|
setValue(value)
|
||||||
dismissDialog()
|
dismissDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clickAction() = editor.clickAction(this)
|
fun checkSafeguard(block: () -> Unit) {
|
||||||
|
if (!option.required && selectionWarningEnabled)
|
||||||
|
showSelectionWarning()
|
||||||
|
else
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clickAction() {
|
||||||
|
checkSafeguard {
|
||||||
|
editor.clickAction(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListItemTrailingContent() = editor.ListItemTrailingContent(this)
|
fun ListItemTrailingContent() = editor.ListItemTrailingContent(this)
|
||||||
@ -113,7 +123,7 @@ private interface OptionEditor<T : Any> {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListItemTrailingContent(scope: OptionEditorScope<T>) {
|
fun ListItemTrailingContent(scope: OptionEditorScope<T>) {
|
||||||
IconButton(onClick = { clickAction(scope) }) {
|
IconButton(onClick = { scope.checkSafeguard { clickAction(scope) } }) {
|
||||||
Icon(Icons.Outlined.Edit, stringResource(R.string.edit))
|
Icon(Icons.Outlined.Edit, stringResource(R.string.edit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,11 +151,14 @@ private inline fun <T : Any> WithOptionEditor(
|
|||||||
option: Option<T>,
|
option: Option<T>,
|
||||||
value: T?,
|
value: T?,
|
||||||
noinline setValue: (T?) -> Unit,
|
noinline setValue: (T?) -> Unit,
|
||||||
|
selectionWarningEnabled: Boolean,
|
||||||
crossinline onDismissDialog: @DisallowComposableCalls () -> Unit = {},
|
crossinline onDismissDialog: @DisallowComposableCalls () -> Unit = {},
|
||||||
block: OptionEditorScope<T>.() -> Unit
|
block: OptionEditorScope<T>.() -> Unit
|
||||||
) {
|
) {
|
||||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
val scope = remember(editor, option, value, setValue) {
|
var showSelectionWarningDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val scope = remember(editor, option, value, setValue, selectionWarningEnabled) {
|
||||||
OptionEditorScope(
|
OptionEditorScope(
|
||||||
editor,
|
editor,
|
||||||
option,
|
option,
|
||||||
@ -154,11 +167,18 @@ private inline fun <T : Any> WithOptionEditor(
|
|||||||
showDialog = false
|
showDialog = false
|
||||||
onDismissDialog()
|
onDismissDialog()
|
||||||
},
|
},
|
||||||
|
selectionWarningEnabled,
|
||||||
|
showSelectionWarning = { showSelectionWarningDialog = true },
|
||||||
value,
|
value,
|
||||||
setValue
|
setValue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showSelectionWarningDialog)
|
||||||
|
SelectionWarningDialog(
|
||||||
|
onDismiss = { showSelectionWarningDialog = false }
|
||||||
|
)
|
||||||
|
|
||||||
if (showDialog) scope.Dialog()
|
if (showDialog) scope.Dialog()
|
||||||
|
|
||||||
scope.block()
|
scope.block()
|
||||||
@ -169,6 +189,7 @@ fun <T : Any> OptionItem(
|
|||||||
option: Option<T>,
|
option: Option<T>,
|
||||||
value: T?,
|
value: T?,
|
||||||
setValue: (T?) -> Unit,
|
setValue: (T?) -> Unit,
|
||||||
|
selectionWarningEnabled: Boolean
|
||||||
) {
|
) {
|
||||||
val editor = remember(option.type, option.presets) {
|
val editor = remember(option.type, option.presets) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@ -181,7 +202,7 @@ fun <T : Any> OptionItem(
|
|||||||
else baseOptionEditor
|
else baseOptionEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
WithOptionEditor(editor, option, value, setValue) {
|
WithOptionEditor(editor, option, value, setValue, selectionWarningEnabled) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable(onClick = ::clickAction),
|
modifier = Modifier.clickable(onClick = ::clickAction),
|
||||||
headlineContent = { Text(option.title) },
|
headlineContent = { Text(option.title) },
|
||||||
@ -300,7 +321,7 @@ private object StringOptionEditor : OptionEditor<String> {
|
|||||||
|
|
||||||
private abstract class NumberOptionEditor<T : Number> : OptionEditor<T> {
|
private abstract class NumberOptionEditor<T : Number> : OptionEditor<T> {
|
||||||
@Composable
|
@Composable
|
||||||
protected abstract fun NumberDialog(
|
abstract fun NumberDialog(
|
||||||
title: String,
|
title: String,
|
||||||
current: T?,
|
current: T?,
|
||||||
validator: (T?) -> Boolean,
|
validator: (T?) -> Boolean,
|
||||||
@ -354,7 +375,14 @@ private object BooleanOptionEditor : OptionEditor<Boolean> {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ListItemTrailingContent(scope: OptionEditorScope<Boolean>) {
|
override fun ListItemTrailingContent(scope: OptionEditorScope<Boolean>) {
|
||||||
HapticSwitch(checked = scope.current, onCheckedChange = scope.setValue)
|
HapticSwitch(
|
||||||
|
checked = scope.current,
|
||||||
|
onCheckedChange = { value ->
|
||||||
|
scope.checkSafeguard {
|
||||||
|
scope.setValue(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -393,6 +421,7 @@ private class PresetOptionEditor<T : Any>(private val innerEditor: OptionEditor<
|
|||||||
scope.option,
|
scope.option,
|
||||||
scope.value,
|
scope.value,
|
||||||
scope.setValue,
|
scope.setValue,
|
||||||
|
scope.selectionWarningEnabled,
|
||||||
onDismissDialog = scope.dismissDialog
|
onDismissDialog = scope.dismissDialog
|
||||||
) inner@{
|
) inner@{
|
||||||
var hidePresetsDialog by rememberSaveable {
|
var hidePresetsDialog by rememberSaveable {
|
||||||
@ -614,7 +643,8 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
|||||||
elementEditor,
|
elementEditor,
|
||||||
elementOption,
|
elementOption,
|
||||||
value = item.value,
|
value = item.value,
|
||||||
setValue = { items[index] = item.copy(value = it) }
|
setValue = { items[index] = item.copy(value = it) },
|
||||||
|
selectionWarningEnabled = scope.selectionWarningEnabled
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.combinedClickable(
|
modifier = Modifier.combinedClickable(
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
package app.revanced.manager.ui.component.patches
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.ui.component.SafeguardDialog
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectionWarningDialog(
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
SafeguardDialog(
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
title = R.string.warning,
|
||||||
|
body = stringResource(R.string.selection_warning_description),
|
||||||
|
)
|
||||||
|
}
|
@ -1,113 +0,0 @@
|
|||||||
package app.revanced.manager.ui.model
|
|
||||||
|
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
|
||||||
import app.revanced.manager.util.PatchSelection
|
|
||||||
import app.revanced.manager.util.flatMapLatestAndCombine
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A data class that contains patch bundle metadata for use by UI code.
|
|
||||||
*/
|
|
||||||
data class BundleInfo(
|
|
||||||
val name: String,
|
|
||||||
val version: String?,
|
|
||||||
val uid: Int,
|
|
||||||
val compatible: List<PatchInfo>,
|
|
||||||
val incompatible: List<PatchInfo>,
|
|
||||||
val universal: List<PatchInfo>
|
|
||||||
) {
|
|
||||||
val all = sequence {
|
|
||||||
yieldAll(compatible)
|
|
||||||
yieldAll(incompatible)
|
|
||||||
yieldAll(universal)
|
|
||||||
}
|
|
||||||
|
|
||||||
val patchCount get() = compatible.size + incompatible.size + universal.size
|
|
||||||
|
|
||||||
fun patchSequence(allowIncompatible: Boolean) = if (allowIncompatible) {
|
|
||||||
all
|
|
||||||
} else {
|
|
||||||
sequence {
|
|
||||||
yieldAll(compatible)
|
|
||||||
yieldAll(universal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object Extensions {
|
|
||||||
inline fun Iterable<BundleInfo>.toPatchSelection(
|
|
||||||
allowIncompatible: Boolean,
|
|
||||||
condition: (Int, PatchInfo) -> Boolean
|
|
||||||
): PatchSelection = this.associate { bundle ->
|
|
||||||
val patches =
|
|
||||||
bundle.patchSequence(allowIncompatible)
|
|
||||||
.mapNotNullTo(mutableSetOf()) { patch ->
|
|
||||||
patch.name.takeIf {
|
|
||||||
condition(
|
|
||||||
bundle.uid,
|
|
||||||
patch
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bundle.uid to patches
|
|
||||||
}
|
|
||||||
|
|
||||||
fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String?) =
|
|
||||||
sources.flatMapLatestAndCombine(
|
|
||||||
combiner = { it.filterNotNull() }
|
|
||||||
) { source ->
|
|
||||||
// Regenerate bundle information whenever this source updates.
|
|
||||||
source.state.map { state ->
|
|
||||||
val bundle = state.patchBundleOrNull() ?: return@map null
|
|
||||||
|
|
||||||
val compatible = mutableListOf<PatchInfo>()
|
|
||||||
val incompatible = mutableListOf<PatchInfo>()
|
|
||||||
val universal = mutableListOf<PatchInfo>()
|
|
||||||
|
|
||||||
bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
|
|
||||||
val targetList = when {
|
|
||||||
it.compatiblePackages == null -> universal
|
|
||||||
it.supports(
|
|
||||||
packageName,
|
|
||||||
version
|
|
||||||
) -> compatible
|
|
||||||
|
|
||||||
else -> incompatible
|
|
||||||
}
|
|
||||||
|
|
||||||
targetList.add(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
BundleInfo(source.getName(), bundle.readManifestAttribute("Version"), source.uid, compatible, incompatible, universal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Algorithm for determining whether all required options have been set.
|
|
||||||
*/
|
|
||||||
inline fun Iterable<BundleInfo>.requiredOptionsSet(
|
|
||||||
crossinline isSelected: (BundleInfo, PatchInfo) -> Boolean,
|
|
||||||
crossinline optionsForPatch: (BundleInfo, PatchInfo) -> Map<String, Any?>?
|
|
||||||
) = all bundle@{ bundle ->
|
|
||||||
bundle
|
|
||||||
.all
|
|
||||||
.filter { isSelected(bundle, it) }
|
|
||||||
.all patch@{
|
|
||||||
if (it.options.isNullOrEmpty()) return@patch true
|
|
||||||
val opts by lazy { optionsForPatch(bundle, it).orEmpty() }
|
|
||||||
|
|
||||||
it.options.all option@{ option ->
|
|
||||||
if (!option.required || option.default != null) return@option true
|
|
||||||
|
|
||||||
option.key in opts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class BundleType {
|
|
||||||
Local,
|
|
||||||
Remote
|
|
||||||
}
|
|
@ -3,59 +3,74 @@ package app.revanced.manager.ui.screen
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.bundle.BundleItem
|
import app.revanced.manager.ui.component.bundle.BundleItem
|
||||||
|
import app.revanced.manager.ui.viewmodel.BundleListViewModel
|
||||||
|
import app.revanced.manager.util.EventEffect
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BundleListScreen(
|
fun BundleListScreen(
|
||||||
onDelete: (PatchBundleSource) -> Unit,
|
viewModel: BundleListViewModel = koinViewModel(),
|
||||||
onUpdate: (PatchBundleSource) -> Unit,
|
eventsFlow: Flow<BundleListViewModel.Event>,
|
||||||
sources: List<PatchBundleSource>,
|
setSelectedSourceCount: (Int) -> Unit
|
||||||
selectedSources: SnapshotStateList<PatchBundleSource>,
|
|
||||||
bundlesSelectable: Boolean,
|
|
||||||
) {
|
) {
|
||||||
val sortedSources = remember(sources) {
|
val patchCounts by viewModel.patchCounts.collectAsStateWithLifecycle(emptyMap())
|
||||||
sources.sortedByDescending { source ->
|
val sources by viewModel.sources.collectAsStateWithLifecycle(emptyList())
|
||||||
source.state.value.patchBundleOrNull()?.patches?.size ?: 0
|
|
||||||
|
EventEffect(eventsFlow) {
|
||||||
|
viewModel.handleEvent(it)
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(viewModel.selectedSources.size) {
|
||||||
|
setSelectedSourceCount(viewModel.selectedSources.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PullToRefreshBox(
|
||||||
|
onRefresh = viewModel::refresh,
|
||||||
|
isRefreshing = viewModel.isRefreshing
|
||||||
|
) {
|
||||||
LazyColumnWithScrollbar(
|
LazyColumnWithScrollbar(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top,
|
verticalArrangement = Arrangement.Top,
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
sortedSources,
|
sources,
|
||||||
key = { it.uid }
|
key = { it.uid }
|
||||||
) { source ->
|
) { source ->
|
||||||
BundleItem(
|
BundleItem(
|
||||||
bundle = source,
|
src = source,
|
||||||
|
patchCount = patchCounts[source.uid] ?: 0,
|
||||||
onDelete = {
|
onDelete = {
|
||||||
onDelete(source)
|
viewModel.delete(source)
|
||||||
},
|
},
|
||||||
onUpdate = {
|
onUpdate = {
|
||||||
onUpdate(source)
|
viewModel.update(source)
|
||||||
},
|
},
|
||||||
selectable = bundlesSelectable,
|
selectable = viewModel.selectedSources.size > 0,
|
||||||
onSelect = {
|
onSelect = {
|
||||||
selectedSources.add(source)
|
viewModel.selectedSources.add(source.uid)
|
||||||
},
|
},
|
||||||
isBundleSelected = selectedSources.contains(source),
|
isBundleSelected = source.uid in viewModel.selectedSources,
|
||||||
toggleSelection = { bundleIsNotSelected ->
|
toggleSelection = { bundleIsNotSelected ->
|
||||||
if (bundleIsNotSelected) {
|
if (bundleIsNotSelected) {
|
||||||
selectedSources.add(source)
|
viewModel.selectedSources.add(source.uid)
|
||||||
} else {
|
} else {
|
||||||
selectedSources.remove(source)
|
viewModel.selectedSources.remove(source.uid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
@ -44,6 +44,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@ -56,7 +57,6 @@ 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.domain.bundles.PatchBundleSource.Extensions.isDefault
|
|
||||||
import app.revanced.manager.patcher.aapt.Aapt
|
import app.revanced.manager.patcher.aapt.Aapt
|
||||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
@ -79,7 +79,7 @@ enum class DashboardPage(
|
|||||||
val icon: ImageVector
|
val icon: ImageVector
|
||||||
) {
|
) {
|
||||||
DASHBOARD(R.string.tab_apps, Icons.Outlined.Apps),
|
DASHBOARD(R.string.tab_apps, Icons.Outlined.Apps),
|
||||||
BUNDLES(R.string.tab_bundles, Icons.Outlined.Source),
|
BUNDLES(R.string.tab_patches, Icons.Outlined.Source),
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("BatteryLife")
|
@SuppressLint("BatteryLife")
|
||||||
@ -93,7 +93,8 @@ fun DashboardScreen(
|
|||||||
onDownloaderPluginClick: () -> Unit,
|
onDownloaderPluginClick: () -> Unit,
|
||||||
onAppClick: (String) -> Unit
|
onAppClick: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
|
var selectedSourceCount by rememberSaveable { mutableIntStateOf(0) }
|
||||||
|
val bundlesSelectable by remember { derivedStateOf { selectedSourceCount > 0 } }
|
||||||
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
||||||
val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle(
|
val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle(
|
||||||
false
|
false
|
||||||
@ -160,12 +161,9 @@ fun DashboardScreen(
|
|||||||
if (showDeleteConfirmationDialog) {
|
if (showDeleteConfirmationDialog) {
|
||||||
ConfirmDialog(
|
ConfirmDialog(
|
||||||
onDismiss = { showDeleteConfirmationDialog = false },
|
onDismiss = { showDeleteConfirmationDialog = false },
|
||||||
onConfirm = {
|
onConfirm = vm::deleteSources,
|
||||||
vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) }
|
title = stringResource(R.string.delete),
|
||||||
vm.cancelSourceSelection()
|
description = stringResource(R.string.patches_delete_multiple_dialog_description),
|
||||||
},
|
|
||||||
title = stringResource(R.string.bundle_delete_multiple_dialog_title),
|
|
||||||
description = stringResource(R.string.bundle_delete_multiple_dialog_description),
|
|
||||||
icon = Icons.Outlined.Delete
|
icon = Icons.Outlined.Delete
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -174,7 +172,7 @@ fun DashboardScreen(
|
|||||||
topBar = {
|
topBar = {
|
||||||
if (bundlesSelectable) {
|
if (bundlesSelectable) {
|
||||||
BundleTopBar(
|
BundleTopBar(
|
||||||
title = stringResource(R.string.bundles_selected, vm.selectedSources.size),
|
title = stringResource(R.string.patches_selected, selectedSourceCount),
|
||||||
onBackClick = vm::cancelSourceSelection,
|
onBackClick = vm::cancelSourceSelection,
|
||||||
backIcon = {
|
backIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
@ -194,10 +192,7 @@ fun DashboardScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = vm::updateSources
|
||||||
vm.selectedSources.forEach { vm.update(it) }
|
|
||||||
vm.cancelSourceSelection()
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Refresh,
|
Icons.Outlined.Refresh,
|
||||||
@ -239,7 +234,7 @@ fun DashboardScreen(
|
|||||||
when (pagerState.currentPage) {
|
when (pagerState.currentPage) {
|
||||||
DashboardPage.DASHBOARD.ordinal -> {
|
DashboardPage.DASHBOARD.ordinal -> {
|
||||||
if (availablePatches < 1) {
|
if (availablePatches < 1) {
|
||||||
androidContext.toast(androidContext.getString(R.string.patches_unavailable))
|
androidContext.toast(androidContext.getString(R.string.no_patch_found))
|
||||||
composableScope.launch {
|
composableScope.launch {
|
||||||
pagerState.animateScrollToPage(
|
pagerState.animateScrollToPage(
|
||||||
DashboardPage.BUNDLES.ordinal
|
DashboardPage.BUNDLES.ordinal
|
||||||
@ -349,18 +344,9 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
|
||||||
|
|
||||||
BundleListScreen(
|
BundleListScreen(
|
||||||
onDelete = {
|
eventsFlow = vm.bundleListEventsFlow,
|
||||||
vm.delete(it)
|
setSelectedSourceCount = { selectedSourceCount = it }
|
||||||
},
|
|
||||||
onUpdate = {
|
|
||||||
vm.update(it)
|
|
||||||
},
|
|
||||||
sources = sources,
|
|
||||||
selectedSources = vm.selectedSources,
|
|
||||||
bundlesSelectable = bundlesSelectable
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,7 @@ import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
|||||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
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.component.patches.SelectionWarningDialog
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_INCOMPATIBLE
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_INCOMPATIBLE
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
||||||
@ -181,7 +182,8 @@ fun PatchesSelectorScreen(
|
|||||||
patch = patch,
|
patch = patch,
|
||||||
values = viewModel.getOptions(bundle, patch),
|
values = viewModel.getOptions(bundle, patch),
|
||||||
reset = { viewModel.resetOptions(bundle, patch) },
|
reset = { viewModel.resetOptions(bundle, patch) },
|
||||||
set = { key, value -> viewModel.setOption(bundle, patch, key, value) }
|
set = { key, value -> viewModel.setOption(bundle, patch, key, value) },
|
||||||
|
selectionWarningEnabled = viewModel.selectionWarningEnabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,9 +217,7 @@ fun PatchesSelectorScreen(
|
|||||||
) { patch ->
|
) { patch ->
|
||||||
PatchItem(
|
PatchItem(
|
||||||
patch = patch,
|
patch = patch,
|
||||||
onOptionsDialog = {
|
onOptionsDialog = { viewModel.optionsDialog = uid to patch },
|
||||||
viewModel.optionsDialog = uid to patch
|
|
||||||
},
|
|
||||||
selected = compatible && viewModel.isSelected(
|
selected = compatible && viewModel.isSelected(
|
||||||
uid,
|
uid,
|
||||||
patch
|
patch
|
||||||
@ -389,6 +389,7 @@ fun PatchesSelectorScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
|
.padding(top = 16.dp)
|
||||||
) {
|
) {
|
||||||
if (bundles.size > 1) {
|
if (bundles.size > 1) {
|
||||||
ScrollableTabRow(
|
ScrollableTabRow(
|
||||||
@ -412,7 +413,7 @@ fun PatchesSelectorScreen(
|
|||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = bundle.version!!,
|
text = bundle.version.orEmpty(),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@ -471,17 +472,6 @@ fun PatchesSelectorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SelectionWarningDialog(
|
|
||||||
onDismiss: () -> Unit
|
|
||||||
) {
|
|
||||||
SafeguardDialog(
|
|
||||||
onDismiss = onDismiss,
|
|
||||||
title = R.string.warning,
|
|
||||||
body = stringResource(R.string.selection_warning_description),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun UniversalPatchWarningDialog(
|
private fun UniversalPatchWarningDialog(
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
@ -611,6 +601,7 @@ private fun OptionsDialog(
|
|||||||
reset: () -> Unit,
|
reset: () -> Unit,
|
||||||
set: (String, Any?) -> Unit,
|
set: (String, Any?) -> Unit,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
|
selectionWarningEnabled: Boolean
|
||||||
) = FullscreenDialog(onDismissRequest = onDismissRequest) {
|
) = FullscreenDialog(onDismissRequest = onDismissRequest) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -641,7 +632,8 @@ private fun OptionsDialog(
|
|||||||
value = value,
|
value = value,
|
||||||
setValue = {
|
setValue = {
|
||||||
set(key, it)
|
set(key, it)
|
||||||
}
|
},
|
||||||
|
selectionWarningEnabled = selectionWarningEnabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,12 +30,12 @@ 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.patcher.patch.Option
|
import app.revanced.manager.patcher.patch.Option
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
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.haptics.HapticExtendedFloatingActionButton
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
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.model.BundleInfo.Extensions.requiredOptionsSet
|
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
@ -62,6 +62,7 @@ fun RequiredOptionsScreen(
|
|||||||
val showContinueButton by remember {
|
val showContinueButton by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
bundles.requiredOptionsSet(
|
bundles.requiredOptionsSet(
|
||||||
|
allowIncompatible = vm.allowIncompatiblePatches,
|
||||||
isSelected = { bundle, patch -> vm.isSelected(bundle.uid, patch) },
|
isSelected = { bundle, patch -> vm.isSelected(bundle.uid, patch) },
|
||||||
optionsForPatch = { bundle, patch -> vm.getOptions(bundle.uid, patch) }
|
optionsForPatch = { bundle, patch -> vm.getOptions(bundle.uid, patch) }
|
||||||
)
|
)
|
||||||
@ -153,7 +154,8 @@ fun RequiredOptionsScreen(
|
|||||||
value = value,
|
value = value,
|
||||||
setValue = { new ->
|
setValue = { new ->
|
||||||
vm.setOption(bundle.uid, it, key, new)
|
vm.setOption(bundle.uid, it, key, new)
|
||||||
}
|
},
|
||||||
|
selectionWarningEnabled = vm.selectionWarningEnabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,13 +48,13 @@ fun DeveloperSettingsScreen(
|
|||||||
description = R.string.developer_options_description,
|
description = R.string.developer_options_description,
|
||||||
)
|
)
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.patch_bundles_section))
|
GroupHeader(stringResource(R.string.patches))
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
headlineContent = stringResource(R.string.patch_bundles_force_download),
|
headlineContent = stringResource(R.string.patches_force_download),
|
||||||
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
|
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
|
||||||
)
|
)
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
headlineContent = stringResource(R.string.patch_bundles_reset),
|
headlineContent = stringResource(R.string.patches_reset),
|
||||||
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
|
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package app.revanced.manager.ui.screen.settings
|
|||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@ -19,9 +18,7 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.material3.pulltorefresh.pullToRefresh
|
|
||||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@ -29,13 +26,11 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
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.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.network.downloader.DownloaderPluginState
|
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||||
@ -57,7 +52,6 @@ fun DownloadsSettingsScreen(
|
|||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
viewModel: DownloadsViewModel = koinViewModel()
|
viewModel: DownloadsViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val pullRefreshState = rememberPullToRefreshState()
|
|
||||||
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
||||||
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
|
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
@ -90,28 +84,13 @@ fun DownloadsSettingsScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Box(
|
PullToRefreshBox(
|
||||||
contentAlignment = Alignment.TopCenter,
|
onRefresh = viewModel::refreshPlugins,
|
||||||
modifier = Modifier
|
|
||||||
.padding(paddingValues)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.zIndex(1f)
|
|
||||||
) {
|
|
||||||
PullToRefreshDefaults.Indicator(
|
|
||||||
state = pullRefreshState,
|
|
||||||
isRefreshing = viewModel.isRefreshingPlugins
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumnWithScrollbar(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues)
|
|
||||||
.pullToRefresh(
|
|
||||||
isRefreshing = viewModel.isRefreshingPlugins,
|
isRefreshing = viewModel.isRefreshingPlugins,
|
||||||
state = pullRefreshState,
|
modifier = Modifier.padding(paddingValues)
|
||||||
onRefresh = viewModel::refreshPlugins
|
) {
|
||||||
)
|
LazyColumnWithScrollbar(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
GroupHeader(stringResource(R.string.downloader_plugins))
|
GroupHeader(stringResource(R.string.downloader_plugins))
|
||||||
@ -241,6 +220,7 @@ fun DownloadsSettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TrustDialog(
|
private fun TrustDialog(
|
||||||
|
@ -227,12 +227,12 @@ fun ImportExportSettingsScreen(
|
|||||||
GroupItem(
|
GroupItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
selectorDialog = {
|
selectorDialog = {
|
||||||
BundleSelector(bundles = patchBundles) { bundle ->
|
BundleSelector(sources = patchBundles) { src ->
|
||||||
bundle?.also {
|
src?.also {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
vm.resetDialogState =
|
vm.resetDialogState =
|
||||||
ResetDialogState.PatchSelectionBundle(bundle.getName()) {
|
ResetDialogState.PatchSelectionBundle(it.name) {
|
||||||
vm.resetSelectionForPatchBundle(bundle)
|
vm.resetSelectionForPatchBundle(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -240,8 +240,8 @@ fun ImportExportSettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
headline = R.string.patch_selection_reset_bundle,
|
headline = R.string.patch_selection_reset_patches,
|
||||||
description = R.string.patch_selection_reset_bundle_description
|
description = R.string.patch_selection_reset_patches_description
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -283,12 +283,12 @@ fun ImportExportSettingsScreen(
|
|||||||
GroupItem(
|
GroupItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
selectorDialog = {
|
selectorDialog = {
|
||||||
BundleSelector(bundles = patchBundles) { bundle ->
|
BundleSelector(sources = patchBundles) { src ->
|
||||||
bundle?.also {
|
src?.also {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
vm.resetDialogState =
|
vm.resetDialogState =
|
||||||
ResetDialogState.PatchOptionBundle(bundle.getName()) {
|
ResetDialogState.PatchOptionBundle(src.name) {
|
||||||
vm.resetOptionsForBundle(bundle)
|
vm.resetOptionsForBundle(src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -296,8 +296,8 @@ fun ImportExportSettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
headline = R.string.patch_options_reset_bundle,
|
headline = R.string.patch_options_reset_patches,
|
||||||
description = R.string.patch_options_reset_bundle_description,
|
description = R.string.patch_options_reset_patches_description,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
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.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.viewmodel.UpdatesSettingsViewModel
|
import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel
|
||||||
@ -50,6 +51,8 @@ fun UpdatesSettingsScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
|
GroupHeader(stringResource(R.string.manager))
|
||||||
|
|
||||||
SettingsListItem(
|
SettingsListItem(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.util.mutableStateSetOf
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.get
|
||||||
|
|
||||||
|
class BundleListViewModel : ViewModel(), KoinComponent {
|
||||||
|
private val patchBundleRepository: PatchBundleRepository = get()
|
||||||
|
val patchCounts = patchBundleRepository.patchCountsFlow
|
||||||
|
var isRefreshing by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
val sources = combine(
|
||||||
|
patchBundleRepository.sources,
|
||||||
|
patchBundleRepository.patchCountsFlow
|
||||||
|
) { sources, patchCounts ->
|
||||||
|
isRefreshing = false
|
||||||
|
sources.sortedByDescending { patchCounts[it.uid] ?: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectedSources = mutableStateSetOf<Int>()
|
||||||
|
|
||||||
|
fun refresh() = viewModelScope.launch {
|
||||||
|
isRefreshing = true
|
||||||
|
patchBundleRepository.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getSelectedSources() = patchBundleRepository.sources
|
||||||
|
.first()
|
||||||
|
.filter { it.uid in selectedSources }
|
||||||
|
.also {
|
||||||
|
selectedSources.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvent(event: Event) {
|
||||||
|
when (event) {
|
||||||
|
Event.CANCEL -> selectedSources.clear()
|
||||||
|
Event.DELETE_SELECTED -> viewModelScope.launch {
|
||||||
|
patchBundleRepository.remove(*getSelectedSources().toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
Event.UPDATE_SELECTED -> viewModelScope.launch {
|
||||||
|
patchBundleRepository.update(
|
||||||
|
*getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(),
|
||||||
|
showToast = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(src: PatchBundleSource) =
|
||||||
|
viewModelScope.launch { patchBundleRepository.remove(src) }
|
||||||
|
|
||||||
|
fun update(src: PatchBundleSource) = viewModelScope.launch {
|
||||||
|
if (src !is RemotePatchBundle) return@launch
|
||||||
|
|
||||||
|
patchBundleRepository.update(src, showToast = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Event {
|
||||||
|
DELETE_SELECTED,
|
||||||
|
UPDATE_SELECTED,
|
||||||
|
CANCEL,
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -24,8 +25,10 @@ import app.revanced.manager.network.api.ReVancedAPI
|
|||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.uiSafe
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class DashboardViewModel(
|
class DashboardViewModel(
|
||||||
@ -38,13 +41,12 @@ class DashboardViewModel(
|
|||||||
private val pm: PM,
|
private val pm: PM,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val availablePatches =
|
val availablePatches =
|
||||||
patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } }
|
patchBundleRepository.bundleInfoFlow.map { it.values.sumOf { bundle -> bundle.patches.size } }
|
||||||
private val contentResolver: ContentResolver = app.contentResolver
|
private val contentResolver: ContentResolver = app.contentResolver
|
||||||
private val powerManager = app.getSystemService<PowerManager>()!!
|
private val powerManager = app.getSystemService<PowerManager>()!!
|
||||||
val sources = patchBundleRepository.sources
|
|
||||||
val selectedSources = mutableStateListOf<PatchBundleSource>()
|
|
||||||
|
|
||||||
val newDownloaderPluginsAvailable = downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() }
|
val newDownloaderPluginsAvailable =
|
||||||
|
downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Android 11 kills the app process after granting the "install apps" permission, which is a problem for the patcher screen.
|
* Android 11 kills the app process after granting the "install apps" permission, which is a problem for the patcher screen.
|
||||||
@ -59,6 +61,9 @@ class DashboardViewModel(
|
|||||||
var showBatteryOptimizationsWarning by mutableStateOf(false)
|
var showBatteryOptimizationsWarning by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
private val bundleListEventsChannel = Channel<BundleListViewModel.Event>()
|
||||||
|
val bundleListEventsFlow = bundleListEventsChannel.receiveAsFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
checkForManagerUpdates()
|
checkForManagerUpdates()
|
||||||
@ -70,10 +75,6 @@ class DashboardViewModel(
|
|||||||
downloaderPluginRepository.acknowledgeAllNewPlugins()
|
downloaderPluginRepository.acknowledgeAllNewPlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissUpdateDialog() {
|
|
||||||
updatedManagerVersion = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun checkForManagerUpdates() {
|
private suspend fun checkForManagerUpdates() {
|
||||||
if (!prefs.managerAutoUpdates.get() || !networkInfo.isConnected()) return
|
if (!prefs.managerAutoUpdates.get() || !networkInfo.isConnected()) return
|
||||||
|
|
||||||
@ -83,7 +84,8 @@ class DashboardViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateBatteryOptimizationsWarning() {
|
fun updateBatteryOptimizationsWarning() {
|
||||||
showBatteryOptimizationsWarning = !powerManager.isIgnoringBatteryOptimizations(app.packageName)
|
showBatteryOptimizationsWarning =
|
||||||
|
!powerManager.isIgnoringBatteryOptimizations(app.packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setShowManagerUpdateDialogOnLaunch(value: Boolean) {
|
fun setShowManagerUpdateDialogOnLaunch(value: Boolean) {
|
||||||
@ -112,36 +114,20 @@ class DashboardViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sendEvent(event: BundleListViewModel.Event) {
|
||||||
fun cancelSourceSelection() {
|
viewModelScope.launch { bundleListEventsChannel.send(event) }
|
||||||
selectedSources.clear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createLocalSource(patchBundle: Uri) =
|
fun cancelSourceSelection() = sendEvent(BundleListViewModel.Event.CANCEL)
|
||||||
viewModelScope.launch {
|
fun updateSources() = sendEvent(BundleListViewModel.Event.UPDATE_SELECTED)
|
||||||
contentResolver.openInputStream(patchBundle)!!.use { patchesStream ->
|
fun deleteSources() = sendEvent(BundleListViewModel.Event.DELETE_SELECTED)
|
||||||
patchBundleRepository.createLocal(patchesStream)
|
|
||||||
}
|
@SuppressLint("Recycle")
|
||||||
|
fun createLocalSource(patchBundle: Uri) = viewModelScope.launch {
|
||||||
|
patchBundleRepository.createLocal { contentResolver.openInputStream(patchBundle)!! }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createRemoteSource(apiUrl: String, autoUpdate: Boolean) =
|
fun createRemoteSource(apiUrl: String, autoUpdate: Boolean) = viewModelScope.launch {
|
||||||
viewModelScope.launch { patchBundleRepository.createRemote(apiUrl, autoUpdate) }
|
patchBundleRepository.createRemote(apiUrl, autoUpdate)
|
||||||
|
|
||||||
fun delete(bundle: PatchBundleSource) =
|
|
||||||
viewModelScope.launch { patchBundleRepository.remove(bundle) }
|
|
||||||
|
|
||||||
fun update(bundle: PatchBundleSource) = viewModelScope.launch {
|
|
||||||
if (bundle !is RemotePatchBundle) return@launch
|
|
||||||
|
|
||||||
uiSafe(
|
|
||||||
app,
|
|
||||||
R.string.source_download_fail,
|
|
||||||
RemotePatchBundle.updateFailMsg
|
|
||||||
) {
|
|
||||||
if (bundle.update())
|
|
||||||
app.toast(app.getString(R.string.bundle_update_success, bundle.getName()))
|
|
||||||
else
|
|
||||||
app.toast(app.getString(R.string.bundle_update_unavailable, bundle.getName()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,7 +16,7 @@ class DeveloperOptionsViewModel(
|
|||||||
private val patchBundleRepository: PatchBundleRepository
|
private val patchBundleRepository: PatchBundleRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
fun redownloadBundles() = viewModelScope.launch {
|
fun redownloadBundles() = viewModelScope.launch {
|
||||||
uiSafe(app, R.string.source_download_fail, RemotePatchBundle.updateFailMsg) {
|
uiSafe(app, R.string.patches_download_fail, RemotePatchBundle.updateFailMsg) {
|
||||||
patchBundleRepository.redownloadRemoteBundles()
|
patchBundleRepository.redownloadRemoteBundles()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,8 +61,8 @@ sealed class ResetDialogState(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class PatchSelectionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
|
class PatchSelectionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
|
||||||
titleResId = R.string.patch_selection_reset_bundle,
|
titleResId = R.string.patch_selection_reset_patches,
|
||||||
descriptionResId = R.string.patch_selection_reset_bundle_dialog_description,
|
descriptionResId = R.string.patch_selection_reset_patches_dialog_description,
|
||||||
onConfirm = onConfirm,
|
onConfirm = onConfirm,
|
||||||
dialogOptionName = dialogOptionName
|
dialogOptionName = dialogOptionName
|
||||||
)
|
)
|
||||||
@ -81,8 +81,8 @@ sealed class ResetDialogState(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class PatchOptionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
|
class PatchOptionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
|
||||||
titleResId = R.string.patch_options_reset_bundle,
|
titleResId = R.string.patch_options_reset_patches,
|
||||||
descriptionResId = R.string.patch_options_reset_bundle_dialog_description,
|
descriptionResId = R.string.patch_options_reset_patches_dialog_description,
|
||||||
onConfirm = onConfirm,
|
onConfirm = onConfirm,
|
||||||
dialogOptionName = dialogOptionName
|
dialogOptionName = dialogOptionName
|
||||||
)
|
)
|
||||||
|
@ -17,10 +17,9 @@ import androidx.lifecycle.viewmodel.compose.saveable
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.ui.model.BundleInfo
|
|
||||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
|
|
||||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
|
||||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
@ -63,7 +62,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
|||||||
val allowIncompatiblePatches =
|
val allowIncompatiblePatches =
|
||||||
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
|
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
|
||||||
val bundlesFlow =
|
val bundlesFlow =
|
||||||
get<PatchBundleRepository>().bundleInfoFlow(packageName, input.app.version)
|
get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.app.version)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@ -76,11 +75,11 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BundleInfo.hasDefaultPatches() =
|
fun PatchBundleInfo.Scoped.hasDefaultPatches() =
|
||||||
patchSequence(allowIncompatiblePatches).any { it.include }
|
patchSequence(allowIncompatiblePatches).any { it.include }
|
||||||
|
|
||||||
// Don't show the warning if there are no default patches.
|
// Don't show the warning if there are no default patches.
|
||||||
selectionWarningEnabled = bundlesFlow.first().any(BundleInfo::hasDefaultPatches)
|
selectionWarningEnabled = bundlesFlow.first().any(PatchBundleInfo.Scoped::hasDefaultPatches)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +122,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
|||||||
// This is for the required options screen.
|
// This is for the required options screen.
|
||||||
private val requiredOptsPatchesDeferred = viewModelScope.async(start = CoroutineStart.LAZY) {
|
private val requiredOptsPatchesDeferred = viewModelScope.async(start = CoroutineStart.LAZY) {
|
||||||
bundlesFlow.first().map { bundle ->
|
bundlesFlow.first().map { bundle ->
|
||||||
bundle to bundle.all.filter { patch ->
|
bundle to bundle.patchSequence(allowIncompatiblePatches).filter { patch ->
|
||||||
val opts by lazy {
|
val opts by lazy {
|
||||||
getOptions(bundle.uid, patch).orEmpty()
|
getOptions(bundle.uid, patch).orEmpty()
|
||||||
}
|
}
|
||||||
@ -136,14 +135,14 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
|||||||
}
|
}
|
||||||
val requiredOptsPatches = flow { emit(requiredOptsPatchesDeferred.await()) }
|
val requiredOptsPatches = flow { emit(requiredOptsPatchesDeferred.await()) }
|
||||||
|
|
||||||
fun selectionIsValid(bundles: List<BundleInfo>) = bundles.any { bundle ->
|
fun selectionIsValid(bundles: List<PatchBundleInfo.Scoped>) = bundles.any { bundle ->
|
||||||
bundle.patchSequence(allowIncompatiblePatches).any { patch ->
|
bundle.patchSequence(allowIncompatiblePatches).any { patch ->
|
||||||
isSelected(bundle.uid, patch)
|
isSelected(bundle.uid, patch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isSelected(bundle: Int, patch: PatchInfo) = customPatchSelection?.let { selection ->
|
fun isSelected(bundle: Int, patch: PatchInfo) = customPatchSelection?.let { selection ->
|
||||||
selection[bundle]?.contains(patch.name) ?: false
|
selection[bundle]?.contains(patch.name) == true
|
||||||
} ?: patch.include
|
} ?: patch.include
|
||||||
|
|
||||||
fun togglePatch(bundle: Int, patch: PatchInfo) = viewModelScope.launch {
|
fun togglePatch(bundle: Int, patch: PatchInfo) = viewModelScope.launch {
|
||||||
|
@ -28,15 +28,14 @@ import app.revanced.manager.domain.repository.InstalledAppRepository
|
|||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
||||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
|
||||||
import app.revanced.manager.plugin.downloader.GetScope
|
import app.revanced.manager.plugin.downloader.GetScope
|
||||||
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.ui.model.BundleInfo
|
|
||||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
|
|
||||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet
|
|
||||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.ui.model.navigation.Patcher
|
import app.revanced.manager.ui.model.navigation.Patcher
|
||||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||||
@ -125,16 +124,19 @@ class SelectedAppInfoViewModel(
|
|||||||
suggestedVersions[input.app.packageName]
|
suggestedVersions[input.app.packageName]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val bundleInfoFlow by derivedStateOf {
|
||||||
|
bundleRepository.scopedBundleInfoFlow(packageName, selectedApp.version)
|
||||||
|
}
|
||||||
|
|
||||||
var options: Options by savedStateHandle.saveable {
|
var options: Options by savedStateHandle.saveable {
|
||||||
val state = mutableStateOf<Options>(emptyMap())
|
val state = mutableStateOf<Options>(emptyMap())
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
||||||
|
val bundlePatches = bundleInfoFlow.first()
|
||||||
|
.associate { it.uid to it.patches.associateBy { patch -> patch.name } }
|
||||||
|
|
||||||
options = withContext(Dispatchers.Default) {
|
options = withContext(Dispatchers.Default) {
|
||||||
val bundlePatches = bundleRepository.bundles.first()
|
|
||||||
.mapValues { (_, bundle) -> bundle.patches.associateBy { it.name } }
|
|
||||||
|
|
||||||
optionsRepository.getOptions(packageName, bundlePatches)
|
optionsRepository.getOptions(packageName, bundlePatches)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,10 +178,6 @@ class SelectedAppInfoViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val bundleInfoFlow by derivedStateOf {
|
|
||||||
bundleRepository.bundleInfoFlow(packageName, selectedApp.version)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showSourceSelector() {
|
fun showSourceSelector() {
|
||||||
dismissSourceSelector()
|
dismissSourceSelector()
|
||||||
showSourceSelector = true
|
showSourceSelector = true
|
||||||
@ -266,9 +264,11 @@ class SelectedAppInfoViewModel(
|
|||||||
selectedAppInfo = info
|
selectedAppInfo = info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getOptionsFiltered(bundles: List<PatchBundleInfo.Scoped>) = options.filtered(bundles)
|
||||||
suspend fun hasSetRequiredOptions(patchSelection: PatchSelection) = bundleInfoFlow
|
suspend fun hasSetRequiredOptions(patchSelection: PatchSelection) = bundleInfoFlow
|
||||||
.first()
|
.first()
|
||||||
.requiredOptionsSet(
|
.requiredOptionsSet(
|
||||||
|
allowIncompatible = prefs.disablePatchVersionCompatCheck.get(),
|
||||||
isSelected = { bundle, patch -> patch.name in patchSelection[bundle.uid]!! },
|
isSelected = { bundle, patch -> patch.name in patchSelection[bundle.uid]!! },
|
||||||
optionsForPatch = { bundle, patch -> options[bundle.uid]?.get(patch.name) },
|
optionsForPatch = { bundle, patch -> options[bundle.uid]?.get(patch.name) },
|
||||||
)
|
)
|
||||||
@ -283,23 +283,23 @@ class SelectedAppInfoViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles)
|
fun getPatches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean) =
|
||||||
|
|
||||||
fun getPatches(bundles: List<BundleInfo>, allowIncompatible: Boolean) =
|
|
||||||
selectionState.patches(bundles, allowIncompatible)
|
selectionState.patches(bundles, allowIncompatible)
|
||||||
|
|
||||||
fun getCustomPatches(
|
fun getCustomPatches(
|
||||||
bundles: List<BundleInfo>,
|
bundles: List<PatchBundleInfo.Scoped>,
|
||||||
allowIncompatible: Boolean
|
allowIncompatible: Boolean
|
||||||
): PatchSelection? =
|
): PatchSelection? =
|
||||||
(selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible)
|
(selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible)
|
||||||
|
|
||||||
fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch {
|
|
||||||
val bundles = bundleInfoFlow.first()
|
|
||||||
|
|
||||||
|
fun updateConfiguration(
|
||||||
|
selection: PatchSelection?,
|
||||||
|
options: Options
|
||||||
|
) = viewModelScope.launch {
|
||||||
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
||||||
|
|
||||||
val filteredOptions = options.filtered(bundles)
|
val filteredOptions = options.filtered(bundleInfoFlow.first())
|
||||||
this@SelectedAppInfoViewModel.options = filteredOptions
|
this@SelectedAppInfoViewModel.options = filteredOptions
|
||||||
|
|
||||||
if (!persistConfiguration) return@launch
|
if (!persistConfiguration) return@launch
|
||||||
@ -319,11 +319,12 @@ class SelectedAppInfoViewModel(
|
|||||||
/**
|
/**
|
||||||
* Returns a copy with all nonexistent options removed.
|
* Returns a copy with all nonexistent options removed.
|
||||||
*/
|
*/
|
||||||
private fun Options.filtered(bundles: List<BundleInfo>): Options = buildMap options@{
|
private fun Options.filtered(bundles: List<PatchBundleInfo.Scoped>): Options =
|
||||||
|
buildMap options@{
|
||||||
bundles.forEach bundles@{ bundle ->
|
bundles.forEach bundles@{ bundle ->
|
||||||
val bundleOptions = this@filtered[bundle.uid] ?: return@bundles
|
val bundleOptions = this@filtered[bundle.uid] ?: return@bundles
|
||||||
|
|
||||||
val patches = bundle.all.associateBy { it.name }
|
val patches = bundle.patches.associateBy { it.name }
|
||||||
|
|
||||||
this@options[bundle.uid] = buildMap bundleOptions@{
|
this@options[bundle.uid] = buildMap bundleOptions@{
|
||||||
bundleOptions.forEach patch@{ (patchName, values) ->
|
bundleOptions.forEach patch@{ (patchName, values) ->
|
||||||
@ -342,11 +343,11 @@ class SelectedAppInfoViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sealed interface SelectionState : Parcelable {
|
private sealed interface SelectionState : Parcelable {
|
||||||
fun patches(bundles: List<BundleInfo>, allowIncompatible: Boolean): PatchSelection
|
fun patches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean): PatchSelection
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Customized(val patchSelection: PatchSelection) : SelectionState {
|
data class Customized(val patchSelection: PatchSelection) : SelectionState {
|
||||||
override fun patches(bundles: List<BundleInfo>, allowIncompatible: Boolean) =
|
override fun patches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean) =
|
||||||
bundles.toPatchSelection(
|
bundles.toPatchSelection(
|
||||||
allowIncompatible
|
allowIncompatible
|
||||||
) { uid, patch ->
|
) { uid, patch ->
|
||||||
@ -356,7 +357,7 @@ private sealed interface SelectionState : Parcelable {
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Default : SelectionState {
|
data object Default : SelectionState {
|
||||||
override fun patches(bundles: List<BundleInfo>, allowIncompatible: Boolean) =
|
override fun patches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean) =
|
||||||
bundles.toPatchSelection(allowIncompatible) { _, patch -> patch.include }
|
bundles.toPatchSelection(allowIncompatible) { _, patch -> patch.include }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,10 +44,10 @@ class PM(
|
|||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
val appList = patchBundleRepository.bundles.map { bundles ->
|
val appList = patchBundleRepository.bundleInfoFlow.map { bundles ->
|
||||||
val compatibleApps = scope.async {
|
val compatibleApps = scope.async {
|
||||||
val compatiblePackages = bundles.values
|
val compatiblePackages = bundles
|
||||||
.flatMap { it.patches }
|
.flatMap { (_, bundle) -> bundle.patches }
|
||||||
.flatMap { it.compatiblePackages.orEmpty() }
|
.flatMap { it.compatiblePackages.orEmpty() }
|
||||||
.groupingBy { it.packageName }
|
.groupingBy { it.packageName }
|
||||||
.eachCount()
|
.eachCount()
|
||||||
|
@ -116,10 +116,10 @@ inline fun LifecycleOwner.launchAndRepeatWithViewLifecycle(
|
|||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
inline fun <T, reified R, C> Flow<Iterable<T>>.flatMapLatestAndCombine(
|
inline fun <T, reified R, C> Flow<Iterable<T>>.flatMapLatestAndCombine(
|
||||||
crossinline combiner: (Array<R>) -> C,
|
crossinline combiner: suspend (Array<R>) -> C,
|
||||||
crossinline transformer: (T) -> Flow<R>,
|
crossinline transformer: suspend (T) -> Flow<R>,
|
||||||
): Flow<C> = flatMapLatest { iterable ->
|
): Flow<C> = flatMapLatest { iterable ->
|
||||||
combine(iterable.map(transformer)) {
|
combine(iterable.map { transformer(it) }) {
|
||||||
combiner(it)
|
combiner(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,26 +14,24 @@
|
|||||||
<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>
|
||||||
<string name="patches_selected">%1$d/%2$d selected</string>
|
<string name="patches_count_selected">%1$d/%2$d selected</string>
|
||||||
|
|
||||||
<string name="new_downloader_plugins_notification">New downloader plugins available. Click here to configure them.</string>
|
<string name="new_downloader_plugins_notification">New downloader plugins available. Click here to configure them.</string>
|
||||||
<string name="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string>
|
<string name="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string>
|
||||||
|
|
||||||
<string name="import_">Import</string>
|
<string name="import_">Import</string>
|
||||||
<string name="import_bundle">Import patch bundle</string>
|
<string name="import_patches">Import patches</string>
|
||||||
<string name="bundle_patches">Bundle patches</string>
|
|
||||||
<string name="patch_bundle_field">Patch bundle</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>
|
||||||
|
|
||||||
<string name="field_not_set">Not set</string>
|
<string name="field_not_set">Not set</string>
|
||||||
|
|
||||||
<string name="bundle_missing">Missing</string>
|
<string name="patches_missing">Missing</string>
|
||||||
<string name="bundle_error">Error</string>
|
<string name="patches_error">Error</string>
|
||||||
<string name="bundle_error_description">Bundle could not be loaded. Click to view the error</string>
|
<string name="patches_error_description">Patches could not be loaded. Click to view the error</string>
|
||||||
<string name="bundle_not_downloaded">Bundle has not been downloaded. Click here to download it</string>
|
<string name="patches_not_downloaded">Patches has not been downloaded. Click here to download it</string>
|
||||||
<string name="bundle_name_default">Default</string>
|
<string name="patches_name_default">Patches</string>
|
||||||
<string name="bundle_name_fallback">Unnamed</string>
|
<string name="patches_name_fallback">Unnamed</string>
|
||||||
|
|
||||||
<string name="android_11_bug_dialog_title">Android 11 bug</string>
|
<string name="android_11_bug_dialog_title">Android 11 bug</string>
|
||||||
<string name="android_11_bug_dialog_description">The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience.</string>
|
<string name="android_11_bug_dialog_description">The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience.</string>
|
||||||
@ -97,8 +95,8 @@
|
|||||||
<string name="suggested_version_safeguard">Require suggested app version</string>
|
<string name="suggested_version_safeguard">Require suggested app version</string>
|
||||||
<string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string>
|
<string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string>
|
||||||
<string name="suggested_version_safeguard_confirmation">Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways?</string>
|
<string name="suggested_version_safeguard_confirmation">Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways?</string>
|
||||||
<string name="patch_selection_safeguard">Allow changing patch selection</string>
|
<string name="patch_selection_safeguard">Allow changing patch selection and options</string>
|
||||||
<string name="patch_selection_safeguard_description">Do not prevent selecting or deselecting patches</string>
|
<string name="patch_selection_safeguard_description">Do not prevent selecting or deselecting patches and customization of options</string>
|
||||||
<string name="patch_selection_safeguard_confirmation">Changing the selection of patches may cause unexpected issues.\n\nEnable anyways?</string>
|
<string name="patch_selection_safeguard_confirmation">Changing the selection of patches may cause unexpected issues.\n\nEnable anyways?</string>
|
||||||
<string name="universal_patches_safeguard">Allow using universal patches</string>
|
<string name="universal_patches_safeguard">Allow using universal patches</string>
|
||||||
<string name="universal_patches_safeguard_description">Do not prevent using universal patches</string>
|
<string name="universal_patches_safeguard_description">Do not prevent using universal patches</string>
|
||||||
@ -133,22 +131,22 @@
|
|||||||
<string name="reset_patch_options">Reset patch options</string>
|
<string name="reset_patch_options">Reset patch options</string>
|
||||||
<string name="reset_patch_options_description">Reset the stored patch options</string>
|
<string name="reset_patch_options_description">Reset the stored patch options</string>
|
||||||
<string name="reset_patch_selection_success">Patch selection has been reset</string>
|
<string name="reset_patch_selection_success">Patch selection has been reset</string>
|
||||||
<string name="patch_selection_reset_all">Reset all patch selection</string>
|
<string name="patch_selection_reset_all">Reset patch selection globally</string>
|
||||||
<string name="patch_selection_reset_all_dialog_description">You are about to reset all the patch selections. You will need to manually select each patch again.</string>
|
<string name="patch_selection_reset_all_dialog_description">You are about to reset all the patch selections. You will need to manually select each patch again.</string>
|
||||||
<string name="patch_selection_reset_all_description">Reset all the patch selections</string>
|
<string name="patch_selection_reset_all_description">Resets all the patch selections</string>
|
||||||
<string name="patch_selection_reset_package">Reset patch selection for app</string>
|
<string name="patch_selection_reset_package">Reset patch selection for app</string>
|
||||||
<string name="patch_selection_reset_package_dialog_description">You are about to reset the patch selection for the app \"%s\". You will have to manually select each patch again.</string>
|
<string name="patch_selection_reset_package_dialog_description">You are about to reset the patch selection for the app \"%s\". You will have to manually select each patch again.</string>
|
||||||
<string name="patch_selection_reset_package_description">Resets patch selection for a single app</string>
|
<string name="patch_selection_reset_package_description">Resets patch selection for a single app</string>
|
||||||
<string name="patch_selection_reset_bundle">Resets patch selection for bundle</string>
|
<string name="patch_selection_reset_patches">Reset patch selection (single)</string>
|
||||||
<string name="patch_selection_reset_bundle_dialog_description">You are about to reset the patch selection for the bundle \"%s\". You will have to manually select each patch again.</string>
|
<string name="patch_selection_reset_patches_dialog_description">You are about to reset the patch selection for \"%s\". You will have to manually select each patch again.</string>
|
||||||
<string name="patch_selection_reset_bundle_description">Resets the patch selection for all patches in a bundle</string>
|
<string name="patch_selection_reset_patches_description">Resets the patch selection for a specific collection of patches</string>
|
||||||
<string name="patch_options_reset_package">Reset patch options for app</string>
|
<string name="patch_options_reset_package">Reset patch options for app</string>
|
||||||
<string name="patch_options_reset_package_dialog_description">You are about to reset the patch options for the app \"%s\". You will have to reapply each option again.</string>
|
<string name="patch_options_reset_package_dialog_description">You are about to reset the patch options for the app \"%s\". You will have to reapply each option again.</string>
|
||||||
<string name="patch_options_reset_package_description">Resets patch options for a single app</string>
|
<string name="patch_options_reset_package_description">Resets patch options for a single app</string>
|
||||||
<string name="patch_options_reset_bundle">Resets patch options for bundle</string>
|
<string name="patch_options_reset_patches">Reset patch options (single)</string>
|
||||||
<string name="patch_options_reset_bundle_dialog_description">You are about to reset the patch options for the bundle \"%s\". You will have to reapply each option again.</string>
|
<string name="patch_options_reset_patches_dialog_description">You are about to reset the patch options for \"%s\". You will have to reapply each option again.</string>
|
||||||
<string name="patch_options_reset_bundle_description">Resets patch options for all patches in a bundle</string>
|
<string name="patch_options_reset_patches_description">Resets the patch options for a specific collection of patches</string>
|
||||||
<string name="patch_options_reset_all">Reset patch options</string>
|
<string name="patch_options_reset_all">Reset patch options globally</string>
|
||||||
<string name="patch_options_reset_all_dialog_description">You are about to reset patch options. You will have to reapply each option again.</string>
|
<string name="patch_options_reset_all_dialog_description">You are about to reset patch options. You will have to reapply each option again.</string>
|
||||||
<string name="patch_options_reset_all_description">Resets all patch options</string>
|
<string name="patch_options_reset_all_description">Resets all patch options</string>
|
||||||
<string name="downloader_plugins">Plugins</string>
|
<string name="downloader_plugins">Plugins</string>
|
||||||
@ -164,7 +162,7 @@
|
|||||||
|
|
||||||
<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 patches…</string>
|
||||||
|
|
||||||
<string name="options">Options</string>
|
<string name="options">Options</string>
|
||||||
<string name="ok">OK</string>
|
<string name="ok">OK</string>
|
||||||
@ -202,8 +200,8 @@
|
|||||||
<string name="debug_logs_export_success">Exported logs</string>
|
<string name="debug_logs_export_success">Exported logs</string>
|
||||||
<string name="api_url">API URL</string>
|
<string name="api_url">API URL</string>
|
||||||
<string name="api_url_description">The API used to download necessary files.</string>
|
<string name="api_url_description">The API used to download necessary files.</string>
|
||||||
<string name="api_url_dialog_title">Set custom API URL</string>
|
<string name="api_url_dialog_title">Change API URL</string>
|
||||||
<string name="api_url_dialog_description">Set the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates.</string>
|
<string name="api_url_dialog_description">Change the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates.</string>
|
||||||
<string name="api_url_dialog_warning">ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it.</string>
|
<string name="api_url_dialog_warning">ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it.</string>
|
||||||
<string name="api_url_dialog_save">Set</string>
|
<string name="api_url_dialog_save">Set</string>
|
||||||
<string name="api_url_dialog_reset">Reset API URL</string>
|
<string name="api_url_dialog_reset">Reset API URL</string>
|
||||||
@ -213,26 +211,25 @@
|
|||||||
<string name="device_architectures">CPU Architectures</string>
|
<string name="device_architectures">CPU Architectures</string>
|
||||||
<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="patches_force_download">Force download all patches</string>
|
||||||
<string name="patch_bundles_force_download">Force download all patch bundles</string>
|
<string name="patches_reset">Reset patches</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>
|
||||||
<string name="storage">Storage</string>
|
<string name="storage">Storage</string>
|
||||||
<string name="patches_unavailable">No patches are available. Check your bundles</string>
|
<string name="no_patch_found">No patch can be found. Check your patches</string>
|
||||||
<string name="tab_apps">Apps</string>
|
<string name="tab_apps">Apps</string>
|
||||||
<string name="tab_bundles">Patch bundles</string>
|
<string name="tab_patches">Patches</string>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
<string name="refresh">Refresh</string>
|
<string name="refresh">Refresh</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="patches_download_fail">Failed to download patches: %s</string>
|
||||||
<string name="source_replace_fail">Failed to load updated patch bundle: %s</string>
|
<string name="patches_replace_fail">Failed to import patches: %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="patches_selected">%s selected</string>
|
||||||
<string name="incompatible_patches">Incompatible patches</string>
|
<string name="incompatible_patches">Incompatible patches</string>
|
||||||
<string name="universal_patches">Universal patches</string>
|
<string name="universal_patches">Universal patches</string>
|
||||||
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
|
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
|
||||||
@ -317,7 +314,8 @@
|
|||||||
<string name="patcher_step_group_saving">Saving</string>
|
<string name="patcher_step_group_saving">Saving</string>
|
||||||
<string name="patcher_step_write_patched">Write patched APK file</string>
|
<string name="patcher_step_write_patched">Write patched APK file</string>
|
||||||
<string name="patcher_step_sign_apk">Sign patched APK file</string>
|
<string name="patcher_step_sign_apk">Sign patched APK file</string>
|
||||||
<string name="patcher_notification_message">Patching in progress…</string>
|
<string name="patcher_notification_title">Patching in progress…</string>
|
||||||
|
<string name="patcher_notification_text">Tap to return to the patcher</string>
|
||||||
<string name="patcher_stop_confirm_title">Stop patcher</string>
|
<string name="patcher_stop_confirm_title">Stop patcher</string>
|
||||||
<string name="patcher_stop_confirm_description">Are you sure you want to stop the patching process?</string>
|
<string name="patcher_stop_confirm_description">Are you sure you want to stop the patching process?</string>
|
||||||
<string name="execute_patches">Execute patches</string>
|
<string name="execute_patches">Execute patches</string>
|
||||||
@ -347,19 +345,13 @@
|
|||||||
<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_source_url">Source URL</string>
|
<string name="patches_update_success">Update successful</string>
|
||||||
<string name="bundle_update_success">Successfully updated %s</string>
|
<string name="patches_update_unavailable">No update available</string>
|
||||||
<string name="bundle_update_unavailable">No update available for %s</string>
|
<string name="view_patches">View patches</string>
|
||||||
<string name="bundle_auto_update">Auto update</string>
|
<string name="patches_view_any_version">Any version</string>
|
||||||
<string name="bundle_auto_update_description">Automatically update this bundle when ReVanced starts</string>
|
<string name="patches_view_any_package">Any package</string>
|
||||||
<string name="bundle_view_patches">View patches</string>
|
<string name="patches_delete_single_dialog_description">Are you sure you want to delete \"%s\"?</string>
|
||||||
<string name="bundle_view_patches_any_version">Any version</string>
|
<string name="patches_delete_multiple_dialog_description">Are you sure you want to delete the selected patches?</string>
|
||||||
<string name="bundle_view_patches_any_package">Any package</string>
|
|
||||||
<string name="bundle_delete_single_dialog_title">Delete bundle</string>
|
|
||||||
<string name="bundle_delete_multiple_dialog_title">Delete bundles</string>
|
|
||||||
<string name="bundle_delete_single_dialog_description">Are you sure you want to delete the bundle \"%s\"?</string>
|
|
||||||
<string name="bundle_delete_multiple_dialog_description">Are you sure you want to delete the selected bundles?</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 Android application that uses ReVanced Patcher to patch Android apps. It allows you to download and patch apps with custom patches, and manage the patching process.</string>
|
<string name="revanced_manager_description">ReVanced Manager is an Android application that uses ReVanced Patcher to patch Android apps. It allows you to download and patch apps with custom patches, and manage the patching process.</string>
|
||||||
@ -413,10 +405,9 @@
|
|||||||
<string name="no_contributors_found">No contributors found</string>
|
<string name="no_contributors_found">No contributors found</string>
|
||||||
<string name="select">Select</string>
|
<string name="select">Select</string>
|
||||||
<string name="select_deselect_all">Select or deselect all</string>
|
<string name="select_deselect_all">Select or deselect all</string>
|
||||||
<string name="select_bundle_type_dialog_title">Add new bundle</string>
|
<string name="select_patches_type_dialog_description">Add new patches from URL or local files</string>
|
||||||
<string name="select_bundle_type_dialog_description">Add a new bundle from a URL or storage</string>
|
<string name="local_patches_description">Add patches from local storage.</string>
|
||||||
<string name="local_bundle_description">Import local files from your storage, does not automatically update</string>
|
<string name="remote_patches_description">Add patches from URL. Patches 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_failed_dialog_title">Installation failed</string>
|
||||||
@ -441,9 +432,10 @@
|
|||||||
<string name="about_device">About device</string>
|
<string name="about_device">About device</string>
|
||||||
<string name="enter_url">Enter URL</string>
|
<string name="enter_url">Enter URL</string>
|
||||||
<string name="next">Next</string>
|
<string name="next">Next</string>
|
||||||
<string name="add_patch_bundle">Add patch bundle</string>
|
|
||||||
<string name="bundle_url">Bundle URL</string>
|
|
||||||
<string name="auto_update">Auto update</string>
|
<string name="auto_update">Auto update</string>
|
||||||
|
<string name="add_patches">Add patches</string>
|
||||||
|
<string name="auto_update_description">Automatically update when a new version is available</string>
|
||||||
|
<string name="patches_url">Patches URL</string>
|
||||||
<string name="incompatible_patches_dialog">These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.</string>
|
<string name="incompatible_patches_dialog">These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.</string>
|
||||||
<string name="incompatible_patch">Incompatible patch</string>
|
<string name="incompatible_patch">Incompatible patch</string>
|
||||||
<string name="any_version">Any</string>
|
<string name="any_version">Any</string>
|
||||||
|
Reference in New Issue
Block a user