Merge branch 'compose-dev' of https://github.com/ReVanced/revanced-manager into fix/minor-issues

This commit is contained in:
Ushie 2024-07-03 03:55:12 +03:00
commit 397a1f8f9c
No known key found for this signature in database
GPG Key ID: B3AAD18842E34632
23 changed files with 194 additions and 125 deletions

View File

@ -29,7 +29,7 @@ android {
buildTypes { buildTypes {
debug { debug {
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
resValue("string", "app_name", "ReVanced Manager Debug") resValue("string", "app_name", "ReVanced Manager (dev)")
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L") buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
} }

View File

@ -9,7 +9,7 @@ interface PatchBundleDao {
suspend fun all(): List<PatchBundleEntity> suspend fun all(): List<PatchBundleEntity>
@Query("SELECT version, integrations_version, auto_update FROM patch_bundles WHERE uid = :uid") @Query("SELECT version, integrations_version, auto_update FROM patch_bundles WHERE uid = :uid")
fun getPropsById(uid: Int): Flow<BundleProperties> fun getPropsById(uid: Int): Flow<BundleProperties?>
@Query("UPDATE patch_bundles SET version = :patches, integrations_version = :integrations WHERE uid = :uid") @Query("UPDATE patch_bundles SET version = :patches, integrations_version = :integrations WHERE uid = :uid")
suspend fun updateVersion(uid: Int, patches: String?, integrations: String?) suspend fun updateVersion(uid: Int, patches: String?, integrations: String?)
@ -17,6 +17,9 @@ interface PatchBundleDao {
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid") @Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
suspend fun setAutoUpdate(uid: Int, value: Boolean) suspend fun setAutoUpdate(uid: Int, value: Boolean)
@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()

View File

@ -7,7 +7,8 @@ import java.io.InputStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSource(name, id, directory) { class LocalPatchBundle(name: String, id: Int, directory: File) :
PatchBundleSource(name, id, directory) {
suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) { suspend fun replace(patches: InputStream? = null, integrations: InputStream? = null) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
patches?.let { inputStream -> patches?.let { inputStream ->
@ -16,10 +17,16 @@ class LocalPatchBundle(name: String, id: Int, directory: File) : PatchBundleSour
} }
} }
integrations?.let { integrations?.let {
Files.copy(it, this@LocalPatchBundle.integrationsFile.toPath(), StandardCopyOption.REPLACE_EXISTING) Files.copy(
it,
this@LocalPatchBundle.integrationsFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
} }
} }
reload() reload()?.also {
saveVersion(it.readManifestAttribute("Version"), null)
}
} }
} }

View File

@ -1,12 +1,20 @@
package app.revanced.manager.domain.bundles package app.revanced.manager.domain.bundles
import android.app.Application
import android.util.Log 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.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 app.revanced.manager.util.tag
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.first
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
@ -14,13 +22,21 @@ import java.io.OutputStream
* A [PatchBundle] source. * A [PatchBundle] source.
*/ */
@Stable @Stable
sealed class PatchBundleSource(val name: String, val uid: Int, directory: File) { sealed class PatchBundleSource(initialName: String, val uid: Int, directory: File) : KoinComponent {
protected val configRepository: PatchBundlePersistenceRepository by inject()
private val app: Application by inject()
protected val patchesFile = directory.resolve("patches.jar") protected val patchesFile = directory.resolve("patches.jar")
protected val integrationsFile = directory.resolve("integrations.apk") protected val integrationsFile = directory.resolve("integrations.apk")
private val _state = MutableStateFlow(load()) private val _state = MutableStateFlow(load())
val state = _state.asStateFlow() val state = _state.asStateFlow()
private val _nameFlow = MutableStateFlow(initialName)
val nameFlow =
_nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.bundle_name_default else R.string.bundle_name_fallback) } }
suspend fun getName() = nameFlow.first()
/** /**
* Returns true if the bundle has been downloaded to local storage. * Returns true if the bundle has been downloaded to local storage.
*/ */
@ -42,13 +58,38 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
return try { return try {
State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists))) State.Loaded(PatchBundle(patchesFile, integrationsFile.takeIf(File::exists)))
} catch (t: Throwable) { } catch (t: Throwable) {
Log.e(tag, "Failed to load patch bundle $name", t) Log.e(tag, "Failed to load patch bundle with UID $uid", t)
State.Failed(t) State.Failed(t)
} }
} }
fun reload() { suspend fun reload(): PatchBundle? {
_state.value = load() 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)
suspend fun getProps() = configRepository.getProps(uid).first()!!
suspend fun currentVersion() = getProps().versionInfo
protected suspend fun saveVersion(patches: String?, integrations: String?) =
configRepository.updateVersion(uid, patches, integrations)
suspend fun setName(name: String) {
configRepository.setName(uid, name)
_nameFlow.value = name
} }
sealed interface State { sealed interface State {
@ -61,9 +102,12 @@ sealed class PatchBundleSource(val name: String, val uid: Int, directory: File)
} }
} }
companion object { companion object Extensions {
val PatchBundleSource.isDefault get() = uid == 0 val PatchBundleSource.isDefault inline get() = uid == 0
val PatchBundleSource.asRemoteOrNull get() = this as? RemotePatchBundle val PatchBundleSource.asRemoteOrNull inline get() = this as? RemotePatchBundle
fun PatchBundleSource.propsOrNullFlow() = asRemoteOrNull?.propsFlow() ?: flowOf(null) val PatchBundleSource.nameState
@Composable inline get() = nameFlow.collectAsStateWithLifecycle(
""
)
} }
} }

View File

@ -2,7 +2,6 @@ package app.revanced.manager.domain.bundles
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import app.revanced.manager.data.room.bundles.VersionInfo import app.revanced.manager.data.room.bundles.VersionInfo
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
import app.revanced.manager.network.dto.BundleAsset import app.revanced.manager.network.dto.BundleAsset
@ -15,17 +14,14 @@ import io.ktor.client.request.url
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
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 @Stable
sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) : sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) :
PatchBundleSource(name, id, directory), KoinComponent { PatchBundleSource(name, id, directory) {
private val configRepository: PatchBundlePersistenceRepository by inject()
protected val http: HttpService by inject() protected val http: HttpService by inject()
protected abstract suspend fun getLatestInfo(): BundleInfo protected abstract suspend fun getLatestInfo(): BundleInfo
@ -70,17 +66,11 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
true true
} }
private suspend fun currentVersion() = configRepository.getProps(uid).first().versionInfo
private suspend fun saveVersion(patches: String, integrations: String) =
configRepository.updateVersion(uid, patches, integrations)
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) { suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
arrayOf(patchesFile, integrationsFile).forEach(File::delete) arrayOf(patchesFile, integrationsFile).forEach(File::delete)
reload() reload()
} }
fun propsFlow() = configRepository.getProps(uid)
suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value) suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value)
companion object { companion object {

View File

@ -5,7 +5,6 @@ import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.bundles.PatchBundleEntity import app.revanced.manager.data.room.bundles.PatchBundleEntity
import app.revanced.manager.data.room.bundles.Source import app.revanced.manager.data.room.bundles.Source
import app.revanced.manager.data.room.bundles.VersionInfo import app.revanced.manager.data.room.bundles.VersionInfo
import io.ktor.http.*
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
class PatchBundlePersistenceRepository(db: AppDatabase) { class PatchBundlePersistenceRepository(db: AppDatabase) {
@ -23,7 +22,6 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
suspend fun reset() = dao.reset() suspend fun reset() = dao.reset()
suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) = suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) =
PatchBundleEntity( PatchBundleEntity(
uid = generateUid(), uid = generateUid(),
@ -37,17 +35,19 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
suspend fun delete(uid: Int) = dao.remove(uid) suspend fun delete(uid: Int) = dao.remove(uid)
suspend fun updateVersion(uid: Int, patches: String, integrations: String) = suspend fun updateVersion(uid: Int, patches: String?, integrations: String?) =
dao.updateVersion(uid, patches, integrations) dao.updateVersion(uid, patches, integrations)
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value) 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() fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged()
private companion object { private companion object {
val defaultSource = PatchBundleEntity( val defaultSource = PatchBundleEntity(
uid = 0, uid = 0,
name = "Main", name = "",
versionInfo = VersionInfo(), versionInfo = VersionInfo(),
source = Source.API, source = Source.API,
autoUpdate = false autoUpdate = false

View File

@ -137,16 +137,16 @@ class PatchBundleRepository(
private fun addBundle(patchBundle: PatchBundleSource) = private fun addBundle(patchBundle: PatchBundleSource) =
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } } _sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }
suspend fun createLocal(name: String, patches: InputStream, integrations: InputStream?) { suspend fun createLocal(patches: InputStream, integrations: InputStream?) {
val id = persistenceRepo.create(name, SourceInfo.Local).uid val uid = persistenceRepo.create("", SourceInfo.Local).uid
val bundle = LocalPatchBundle(name, id, directoryOf(id)) val bundle = LocalPatchBundle("", uid, directoryOf(uid))
bundle.replace(patches, integrations) bundle.replace(patches, integrations)
addBundle(bundle) addBundle(bundle)
} }
suspend fun createRemote(name: String, url: String, autoUpdate: Boolean) { suspend fun createRemote(url: String, autoUpdate: Boolean) {
val entity = persistenceRepo.create(name, SourceInfo.from(url), autoUpdate) val entity = persistenceRepo.create("", SourceInfo.from(url), autoUpdate)
addBundle(entity.load()) addBundle(entity.load())
} }
@ -174,8 +174,8 @@ class PatchBundleRepository(
getBundlesByType<RemotePatchBundle>().forEach { getBundlesByType<RemotePatchBundle>().forEach {
launch { launch {
if (!it.propsFlow().first().autoUpdate) return@launch if (!it.getProps().autoUpdate) return@launch
Log.d(tag, "Updating patch bundle: ${it.name}") Log.d(tag, "Updating patch bundle: ${it.getName()}")
it.update() it.update()
} }
} }

View File

@ -47,7 +47,7 @@ class Session(
var nextPatchIndex = 0 var nextPatchIndex = 0
updateProgress( updateProgress(
name = androidContext.getString(R.string.applying_patch, selectedPatches[nextPatchIndex]), name = androidContext.getString(R.string.executing_patch, selectedPatches[nextPatchIndex]),
state = State.RUNNING state = State.RUNNING
) )
@ -56,7 +56,7 @@ class Session(
if (exception != null) { if (exception != null) {
updateProgress( updateProgress(
name = androidContext.getString(R.string.failed_to_apply_patch, patch.name), name = androidContext.getString(R.string.failed_to_execute_patch, patch.name),
state = State.FAILED, state = State.FAILED,
message = exception.stackTraceToString() message = exception.stackTraceToString()
) )
@ -72,7 +72,7 @@ class Session(
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch -> selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
updateProgress( updateProgress(
name = androidContext.getString(R.string.applying_patch, nextPatch.name) name = androidContext.getString(R.string.executing_patch, nextPatch.name)
) )
} }
@ -82,7 +82,7 @@ class Session(
updateProgress( updateProgress(
state = State.COMPLETED, state = State.COMPLETED,
name = androidContext.resources.getQuantityString( name = androidContext.resources.getQuantityString(
R.plurals.patches_applied, R.plurals.patches_executed,
selectedPatches.size, selectedPatches.size,
selectedPatches.size selectedPatches.size
) )
@ -105,7 +105,6 @@ class Session(
logger.info("Merging integrations") logger.info("Merging integrations")
acceptIntegrations(integrations.toSet()) acceptIntegrations(integrations.toSet())
acceptPatches(selectedPatches.toSet()) acceptPatches(selectedPatches.toSet())
updateProgress(state = State.COMPLETED) // Merging
logger.info("Applying patches...") logger.info("Applying patches...")
applyPatchesVerbose(selectedPatches.sortedBy { it.name }) applyPatchesVerbose(selectedPatches.sortedBy { it.name })

View File

@ -5,6 +5,8 @@ import app.revanced.manager.util.tag
import app.revanced.patcher.PatchBundleLoader import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import java.io.File import java.io.File
import java.io.IOException
import java.util.jar.JarFile
class PatchBundle(val patchesJar: File, val integrations: File?) { class PatchBundle(val patchesJar: File, val integrations: File?) {
private val loader = object : Iterable<Patch<*>> { private val loader = object : Iterable<Patch<*>> {
@ -25,6 +27,17 @@ class PatchBundle(val patchesJar: File, val integrations: File?) {
*/ */
val patches = loader.map(::PatchInfo) val patches = loader.map(::PatchInfo)
/**
* The [java.util.jar.Manifest] of [patchesJar].
*/
private val manifest = try {
JarFile(patchesJar).use { it.manifest }
} catch (_: IOException) {
null
}
fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name)
/** /**
* Load all patches compatible with the specified package. * Load all patches compatible with the specified package.
*/ */

View File

@ -184,11 +184,11 @@ class PatcherWorker(
Log.i(tag, "Patching succeeded".logFmt()) Log.i(tag, "Patching succeeded".logFmt())
Result.success() Result.success()
} catch (e: ProcessRuntime.RemoteFailureException) { } catch (e: ProcessRuntime.RemoteFailureException) {
Log.e(tag, "An exception occured in the remote process while patching. ${e.originalStackTrace}".logFmt()) Log.e(tag, "An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt())
updateProgress(state = State.FAILED, message = e.originalStackTrace) updateProgress(state = State.FAILED, message = e.originalStackTrace)
Result.failure() Result.failure()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "An exception occured while patching".logFmt(), e) Log.e(tag, "An exception occurred while patching".logFmt(), e)
updateProgress(state = State.FAILED, message = e.stackTraceToString()) updateProgress(state = State.FAILED, message = e.stackTraceToString())
Result.failure() Result.failure()
} finally { } finally {

View File

@ -30,7 +30,7 @@ import app.revanced.manager.util.isDebuggable
fun BaseBundleDialog( fun BaseBundleDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isDefault: Boolean, isDefault: Boolean,
name: String, name: String?,
onNameChange: ((String) -> Unit)? = null, onNameChange: ((String) -> Unit)? = null,
remoteUrl: String?, remoteUrl: String?,
onRemoteUrlChange: ((String) -> Unit)? = null, onRemoteUrlChange: ((String) -> Unit)? = null,
@ -52,32 +52,34 @@ fun BaseBundleDialog(
) )
.then(modifier) .then(modifier)
) { ) {
var showNameInputDialog by rememberSaveable { if (name != null) {
mutableStateOf(false) var showNameInputDialog by rememberSaveable {
} mutableStateOf(false)
if (showNameInputDialog) { }
TextInputDialog( if (showNameInputDialog) {
initial = name, TextInputDialog(
title = stringResource(R.string.bundle_input_name), initial = name,
onDismissRequest = { title = stringResource(R.string.bundle_input_name),
showNameInputDialog = false onDismissRequest = {
}, showNameInputDialog = false
onConfirm = { },
showNameInputDialog = false onConfirm = {
onNameChange?.invoke(it) showNameInputDialog = false
}, onNameChange?.invoke(it)
validator = { },
it.length in 1..19 validator = {
it.length in 1..19
}
)
}
BundleListItem(
headlineText = stringResource(R.string.bundle_input_name),
supportingText = name.ifEmpty { stringResource(R.string.field_not_set) },
modifier = Modifier.clickable(enabled = onNameChange != null) {
showNameInputDialog = true
} }
) )
} }
BundleListItem(
headlineText = stringResource(R.string.bundle_input_name),
supportingText = name.ifEmpty { stringResource(R.string.field_not_set) },
modifier = Modifier.clickable(enabled = onNameChange != null) {
showNameInputDialog = true
}
)
remoteUrl?.takeUnless { isDefault }?.let { url -> remoteUrl?.takeUnless { isDefault }?.let { url ->
var showUrlInputDialog by rememberSaveable { var showUrlInputDialog by rememberSaveable {

View File

@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.DeleteOutline
import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -23,9 +23,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
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.Companion.asRemoteOrNull import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.propsOrNullFlow import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -44,7 +44,7 @@ fun BundleInformationDialog(
bundle.state.map { it.patchBundleOrNull()?.patches?.size ?: 0 } bundle.state.map { it.patchBundleOrNull()?.patches?.size ?: 0 }
}.collectAsStateWithLifecycle(0) }.collectAsStateWithLifecycle(0)
val props by remember(bundle) { val props by remember(bundle) {
bundle.propsOrNullFlow() bundle.propsFlow()
}.collectAsStateWithLifecycle(null) }.collectAsStateWithLifecycle(null)
if (viewCurrentBundlePatches) { if (viewCurrentBundlePatches) {
@ -63,10 +63,12 @@ fun BundleInformationDialog(
dismissOnBackPress = true dismissOnBackPress = true
) )
) { ) {
val bundleName by bundle.nameState
Scaffold( Scaffold(
topBar = { topBar = {
BundleTopBar( BundleTopBar(
title = bundle.name, title = bundleName,
onBackClick = onDismissRequest, onBackClick = onDismissRequest,
onBackIcon = { onBackIcon = {
Icon( Icon(
@ -86,7 +88,7 @@ fun BundleInformationDialog(
if (!isLocal) { if (!isLocal) {
IconButton(onClick = onRefreshButton) { IconButton(onClick = onRefreshButton) {
Icon( Icon(
Icons.Outlined.Refresh, Icons.Outlined.Update,
stringResource(R.string.refresh) stringResource(R.string.refresh)
) )
} }
@ -98,7 +100,8 @@ fun BundleInformationDialog(
BaseBundleDialog( BaseBundleDialog(
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
isDefault = bundle.isDefault, isDefault = bundle.isDefault,
name = bundle.name, name = bundleName,
onNameChange = { composableScope.launch { bundle.setName(it) } },
remoteUrl = bundle.asRemoteOrNull?.endpoint, remoteUrl = bundle.asRemoteOrNull?.endpoint,
patchCount = patchCount, patchCount = patchCount,
version = props?.versionInfo?.patches, version = props?.versionInfo?.patches,

View File

@ -27,7 +27,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.propsOrNullFlow import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -45,8 +45,9 @@ fun BundleItem(
val state by bundle.state.collectAsStateWithLifecycle() val state by bundle.state.collectAsStateWithLifecycle()
val version by remember(bundle) { val version by remember(bundle) {
bundle.propsOrNullFlow().map { props -> props?.versionInfo?.patches } bundle.propsFlow().map { props -> props?.versionInfo?.patches }
}.collectAsStateWithLifecycle(null) }.collectAsStateWithLifecycle(null)
val name by bundle.nameState
if (viewBundleDialogPage) { if (viewBundleDialogPage) {
BundleInformationDialog( BundleInformationDialog(
@ -77,10 +78,10 @@ fun BundleItem(
} }
} else null, } else null,
headlineContent = { Text(text = bundle.name) }, headlineContent = { Text(name) },
supportingContent = { supportingContent = {
state.patchBundleOrNull()?.patches?.size?.let { patchCount -> state.patchBundleOrNull()?.patches?.size?.let { patchCount ->
Text(text = pluralStringResource(R.plurals.patch_count, patchCount, patchCount)) Text(pluralStringResource(R.plurals.patch_count, patchCount, patchCount))
} }
}, },
trailingContent = { trailingContent = {
@ -95,7 +96,7 @@ fun BundleItem(
icon?.let { (vector, description) -> icon?.let { (vector, description) ->
Icon( Icon(
imageVector = vector, vector,
contentDescription = stringResource(description), contentDescription = stringResource(description),
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.error tint = MaterialTheme.colorScheme.error

View File

@ -12,10 +12,12 @@ 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.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -51,6 +53,7 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
) )
} }
bundles.forEach { bundles.forEach {
val name by it.nameState
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
@ -62,7 +65,7 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
} }
) { ) {
Text( Text(
text = it.name, name,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )

View File

@ -44,8 +44,8 @@ import app.revanced.manager.util.JAR_MIMETYPE
@Composable @Composable
fun ImportPatchBundleDialog( fun ImportPatchBundleDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onRemoteSubmit: (String, String, Boolean) -> Unit, onRemoteSubmit: (String, Boolean) -> Unit,
onLocalSubmit: (String, Uri, Uri?) -> Unit onLocalSubmit: (Uri, Uri?) -> Unit
) { ) {
var currentStep by rememberSaveable { mutableIntStateOf(0) } var currentStep by rememberSaveable { mutableIntStateOf(0) }
var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) } var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) }
@ -59,11 +59,19 @@ fun ImportPatchBundleDialog(
uri?.let { patchBundle = it } uri?.let { patchBundle = it }
} }
fun launchPatchActivity() {
patchActivityLauncher.launch(JAR_MIMETYPE)
}
val integrationsActivityLauncher = val integrationsActivityLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { integrations = it } uri?.let { integrations = it }
} }
fun launchIntegrationsActivity() {
integrationsActivityLauncher.launch(APK_MIMETYPE)
}
val steps = listOf<@Composable () -> Unit>( val steps = listOf<@Composable () -> Unit>(
{ {
SelectBundleTypeStep(bundleType) { selectedType -> SelectBundleTypeStep(bundleType) { selectedType ->
@ -77,8 +85,8 @@ fun ImportPatchBundleDialog(
integrations, integrations,
remoteUrl, remoteUrl,
autoUpdate, autoUpdate,
{ patchActivityLauncher.launch(JAR_MIMETYPE) }, { launchPatchActivity() },
{ integrationsActivityLauncher.launch(APK_MIMETYPE) }, { launchIntegrationsActivity() },
{ remoteUrl = it }, { remoteUrl = it },
{ autoUpdate = it } { autoUpdate = it }
) )
@ -108,13 +116,12 @@ fun ImportPatchBundleDialog(
when (bundleType) { when (bundleType) {
BundleType.Local -> patchBundle?.let { BundleType.Local -> patchBundle?.let {
onLocalSubmit( onLocalSubmit(
"BundleName",
it, it,
integrations integrations
) )
} }
BundleType.Remote -> onRemoteSubmit("BundleName", remoteUrl, autoUpdate) BundleType.Remote -> onRemoteSubmit(remoteUrl, autoUpdate)
} }
} }
) { ) {

View File

@ -75,7 +75,7 @@ data class BundleInfo(
targetList.add(it) targetList.add(it)
} }
BundleInfo(source.name, source.uid, supported, unsupported, universal) BundleInfo(source.getName(), source.uid, supported, unsupported, universal)
} }
} }
} }

View File

@ -46,7 +46,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault 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.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.component.AutoUpdatesDialog
@ -96,13 +96,13 @@ fun DashboardScreen(
if (showAddBundleDialog) { if (showAddBundleDialog) {
ImportPatchBundleDialog( ImportPatchBundleDialog(
onDismiss = { showAddBundleDialog = false }, onDismiss = { showAddBundleDialog = false },
onLocalSubmit = { name, patches, integrations -> onLocalSubmit = { patches, integrations ->
showAddBundleDialog = false showAddBundleDialog = false
vm.createLocalSource(name, patches, integrations) vm.createLocalSource(patches, integrations)
}, },
onRemoteSubmit = { name, url, autoUpdate -> onRemoteSubmit = { url, autoUpdate ->
showAddBundleDialog = false showAddBundleDialog = false
vm.createRemoteSource(name, url, autoUpdate) vm.createRemoteSource(url, autoUpdate)
} }
) )
} }

View File

@ -9,8 +9,8 @@ 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
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -94,6 +94,8 @@ fun PatchesSelectorScreen(
derivedStateOf { vm.selectionIsValid(bundles) } derivedStateOf { vm.selectionIsValid(bundles) }
} }
val patchLazyListStates = remember(bundles) { List(bundles.size) { LazyListState() } }
if (showBottomSheet) { if (showBottomSheet) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { onDismissRequest = {
@ -255,7 +257,6 @@ fun PatchesSelectorScreen(
} }
} }
val patchLazyListState = rememberLazyListState()
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
@ -284,7 +285,7 @@ fun PatchesSelectorScreen(
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { Text(stringResource(R.string.save)) }, text = { Text(stringResource(R.string.save)) },
icon = { Icon(Icons.Outlined.Save, null) }, icon = { Icon(Icons.Outlined.Save, null) },
expanded = patchLazyListState.isScrollingUp, expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp ?: true,
onClick = { onClick = {
// TODO: only allow this if all required options have been set. // TODO: only allow this if all required options have been set.
onSave(vm.getCustomSelection(), vm.getOptions()) onSave(vm.getCustomSelection(), vm.getOptions())
@ -324,11 +325,13 @@ fun PatchesSelectorScreen(
state = pagerState, state = pagerState,
userScrollEnabled = true, userScrollEnabled = true,
pageContent = { index -> pageContent = { index ->
// Avoid crashing if the lists have not been fully initialized yet.
if (index > bundles.lastIndex || bundles.size != patchLazyListStates.size) return@HorizontalPager
val bundle = bundles[index] val bundle = bundles[index]
LazyColumnWithScrollbar( LazyColumnWithScrollbar(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = patchLazyListState state = patchLazyListStates[index]
) { ) {
patchList( patchList(
uid = bundle.uid, uid = bundle.uid,

View File

@ -12,7 +12,7 @@ import androidx.lifecycle.viewModelScope
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.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.bundles.RemotePatchBundle import app.revanced.manager.domain.bundles.RemotePatchBundle
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
@ -80,20 +80,17 @@ class DashboardViewModel(
fun cancelSourceSelection() { fun cancelSourceSelection() {
selectedSources.clear() selectedSources.clear()
} }
fun createLocalSource(name: String, patchBundle: Uri, integrations: Uri?) = fun createLocalSource(patchBundle: Uri, integrations: Uri?) =
viewModelScope.launch { viewModelScope.launch {
contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> contentResolver.openInputStream(patchBundle)!!.use { patchesStream ->
val integrationsStream = integrations?.let { contentResolver.openInputStream(it) } integrations?.let { contentResolver.openInputStream(it) }.use { integrationsStream ->
try { patchBundleRepository.createLocal(patchesStream, integrationsStream)
patchBundleRepository.createLocal(name, patchesStream, integrationsStream)
} finally {
integrationsStream?.close()
} }
} }
} }
fun createRemoteSource(name: String, apiUrl: String, autoUpdate: Boolean) = fun createRemoteSource(apiUrl: String, autoUpdate: Boolean) =
viewModelScope.launch { patchBundleRepository.createRemote(name, apiUrl, autoUpdate) } viewModelScope.launch { patchBundleRepository.createRemote(apiUrl, autoUpdate) }
fun delete(bundle: PatchBundleSource) = fun delete(bundle: PatchBundleSource) =
viewModelScope.launch { patchBundleRepository.remove(bundle) } viewModelScope.launch { patchBundleRepository.remove(bundle) }
@ -107,9 +104,9 @@ class DashboardViewModel(
RemotePatchBundle.updateFailMsg RemotePatchBundle.updateFailMsg
) { ) {
if (bundle.update()) if (bundle.update())
app.toast(app.getString(R.string.bundle_update_success, bundle.name)) app.toast(app.getString(R.string.bundle_update_success, bundle.getName()))
else else
app.toast(app.getString(R.string.bundle_update_unavailable, bundle.name)) app.toast(app.getString(R.string.bundle_update_unavailable, bundle.getName()))
} }
} }
} }

View File

@ -11,7 +11,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.manager.KeystoreManager import app.revanced.manager.domain.manager.KeystoreManager
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

View File

@ -316,13 +316,9 @@ class PatcherViewModel(
context.getString(R.string.patcher_step_unpack), context.getString(R.string.patcher_step_unpack),
StepCategory.PREPARING StepCategory.PREPARING
), ),
Step(
context.getString(R.string.patcher_step_integrations),
StepCategory.PREPARING
),
Step( Step(
context.getString(R.string.apply_patches), context.getString(R.string.execute_patches),
StepCategory.PATCHING StepCategory.PATCHING
), ),

View File

@ -4,8 +4,8 @@
<item quantity="one">%d patch</item> <item quantity="one">%d patch</item>
<item quantity="other">%d patches</item> <item quantity="other">%d patches</item>
</plurals> </plurals>
<plurals name="patches_applied"> <plurals name="patches_executed">
<item quantity="one">Applied %d patch</item> <item quantity="one">Executed %d patch</item>
<item quantity="other">Applied %d patches</item> <item quantity="other">Executed %d patches</item>
</plurals> </plurals>
</resources> </resources>

View File

@ -25,6 +25,8 @@
<string name="bundle_missing">Missing</string> <string name="bundle_missing">Missing</string>
<string name="bundle_error">Error</string> <string name="bundle_error">Error</string>
<string name="bundle_name_default">Default</string>
<string name="bundle_name_fallback">Unnamed</string>
<string name="selected_app_meta">%1$s • %2$d available patches</string> <string name="selected_app_meta">%1$s • %2$d available patches</string>
@ -162,7 +164,7 @@
<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</string> <string name="download_apk">Download APK file</string>
<string name="source_download_fail">Failed to download patch bundle: %s</string> <string name="source_download_fail">Failed to download patch bundle: %s</string>
<string name="source_replace_fail">Failed to load updated patch bundle: %s</string> <string name="source_replace_fail">Failed to load updated patch bundle: %s</string>
<string name="source_replace_integrations_fail">Failed to update integrations: %s</string> <string name="source_replace_integrations_fail">Failed to update integrations: %s</string>
@ -248,16 +250,15 @@
<string name="patcher_step_group_preparing">Preparing</string> <string name="patcher_step_group_preparing">Preparing</string>
<string name="patcher_step_load_patches">Load patches</string> <string name="patcher_step_load_patches">Load patches</string>
<string name="patcher_step_unpack">Unpack APK</string> <string name="patcher_step_unpack">Read APK file</string>
<string name="patcher_step_integrations">Merge Integrations</string>
<string name="patcher_step_group_patching">Patching</string> <string name="patcher_step_group_patching">Patching</string>
<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</string> <string name="patcher_step_write_patched">Write patched APK file</string>
<string name="patcher_step_sign_apk">Sign APK</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_message">Patching in progress…</string>
<string name="apply_patches">Apply patches</string> <string name="execute_patches">Execute patches</string>
<string name="applying_patch">Applying %s</string> <string name="executing_patch">Execute %s</string>
<string name="failed_to_apply_patch">Failed to apply %s</string> <string name="failed_to_execute_patch">Failed to execute %s</string>
<string name="step_completed">completed</string> <string name="step_completed">completed</string>
<string name="step_failed">failed</string> <string name="step_failed">failed</string>