mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-03-13 08:41:57 +08:00
Compare commits
14 Commits
v1.26.0-de
...
v1.26.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b38d0b7022 | ||
|
|
b838851ebe | ||
|
|
00b3c59572 | ||
|
|
fc30ba5e83 | ||
|
|
32228404c4 | ||
|
|
f19080a986 | ||
|
|
813df46847 | ||
|
|
6f4219c01b | ||
|
|
8667051283 | ||
|
|
e0ce6b4ef3 | ||
|
|
b50688ee10 | ||
|
|
31a0f751aa | ||
|
|
4f65555b56 | ||
|
|
4fc2ba339b |
@@ -1,3 +1,10 @@
|
||||
# api [1.0.0-dev.2](https://github.com/ReVanced/revanced-manager/compare/api@1.0.0-dev.1...api@1.0.0-dev.2) (2026-02-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to Patcher v22 ([#2939](https://github.com/ReVanced/revanced-manager/issues/2939)) ([8667051](https://github.com/ReVanced/revanced-manager/commit/8667051283f934a32ac7b7cc76178397dc45a0e6))
|
||||
|
||||
# api 1.0.0-dev.1 (2026-02-19)
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ kotlin {
|
||||
jvmToolchain(17)
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xexplicit-backing-fields",
|
||||
"-Xcontext-parameters",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 1.0.0-dev.1
|
||||
version = 1.0.0-dev.2
|
||||
|
||||
@@ -1,3 +1,34 @@
|
||||
# app [1.26.0-dev.24](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.23...v1.26.0-dev.24) (2026-02-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Incorrect patcher string value ([f19080a](https://github.com/ReVanced/revanced-manager/commit/f19080a9862be4600860b67cd981b9a3a56b21e7))
|
||||
* Never show again toggle doing nothing for Update dialog ([6f4219c](https://github.com/ReVanced/revanced-manager/commit/6f4219c01ba43b70e130f210c8d48978a6056f68))
|
||||
* Release builds not working ([00b3c59](https://github.com/ReVanced/revanced-manager/commit/00b3c59572760993f69f2663456237faafff0283))
|
||||
* Remove quotes from strings ([fc30ba5](https://github.com/ReVanced/revanced-manager/commit/fc30ba5e83f27329b05dc81f484bb8f7471b6a99))
|
||||
* we actually don't want to exclude reflection symbols ([3222840](https://github.com/ReVanced/revanced-manager/commit/32228404c4ef38bb8931d5cfdf6a7aefde23a89c))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add announcements ([#2948](https://github.com/ReVanced/revanced-manager/issues/2948)) ([813df46](https://github.com/ReVanced/revanced-manager/commit/813df46847620bd854dccc52ff244388b3385e17))
|
||||
* Update to Patcher v22 ([#2939](https://github.com/ReVanced/revanced-manager/issues/2939)) ([8667051](https://github.com/ReVanced/revanced-manager/commit/8667051283f934a32ac7b7cc76178397dc45a0e6))
|
||||
|
||||
# app [1.26.0-dev.23](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.22...v1.26.0-dev.23) (2026-02-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Flutter migration ([#2946](https://github.com/ReVanced/revanced-manager/issues/2946)) ([31a0f75](https://github.com/ReVanced/revanced-manager/commit/31a0f751aaff3233f50821963f28454d239318b2))
|
||||
|
||||
# app [1.26.0-dev.22](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.21...v1.26.0-dev.22) (2026-02-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Create modules path in root installer ([4fc2ba3](https://github.com/ReVanced/revanced-manager/commit/4fc2ba339b6d275fb9da8b65ff86b74a6594436d))
|
||||
|
||||
# app [1.26.0-dev.21](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.20...v1.26.0-dev.21) (2026-02-19)
|
||||
|
||||
|
||||
|
||||
@@ -135,15 +135,15 @@ android {
|
||||
buildToolsVersion = "35.0.1"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.revanced.manager"
|
||||
applicationId = "app.revanced.manager.flutter"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
|
||||
val versionStr = if (version == "unspecified") "1.0.0" else version.toString()
|
||||
versionName = versionStr
|
||||
versionCode = with(versionStr.toVersion()) {
|
||||
major * 10_000_000 +
|
||||
minor * 10_000 +
|
||||
major * 100_000_000 +
|
||||
minor * 100_000 +
|
||||
patch * 100 +
|
||||
(preRelease?.substringAfterLast('.')?.toInt() ?: 99)
|
||||
}
|
||||
@@ -217,23 +217,6 @@ android {
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes.addAll(
|
||||
listOf(
|
||||
"/prebuilt/**",
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/**.version",
|
||||
"DebugProbesKt.bin",
|
||||
"kotlin-tooling-metadata.json",
|
||||
"org/bouncycastle/pqc/**.properties",
|
||||
"org/bouncycastle/x509/**.properties",
|
||||
)
|
||||
)
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
@@ -254,12 +237,66 @@ android {
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
// Useless files
|
||||
excludes += "/XPP3_*_VERSION"
|
||||
excludes += "/font-awesome-license.txt"
|
||||
excludes += "/smali.properties"
|
||||
excludes += "/baksmali.properties"
|
||||
excludes += "/properties/apktool.properties"
|
||||
excludes += "/org/antlr/**"
|
||||
excludes += "/org/mockito/**"
|
||||
excludes += "/org/bouncycastle/pqc/**.properties"
|
||||
excludes += "/org/bouncycastle/x509/**.properties"
|
||||
excludes += "/META-INF/INDEX.LIST"
|
||||
excludes += "/META-INF/**/*.txt"
|
||||
excludes += "/META-INF/**/*.properties"
|
||||
excludes += "/META-INF/DEPENDENCIES"
|
||||
|
||||
// Desktop AAPT binaries
|
||||
excludes += "/prebuilt/**"
|
||||
}
|
||||
jniLibs {
|
||||
// 32-bit x86 is dead
|
||||
excludes += "/lib/x86/*.so"
|
||||
|
||||
// Equivalent of AndroidManifest's extractNativeLibs=true to ensure libs are compressed
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants(selector().withBuildType("release")) {
|
||||
it.packaging.resources.excludes.apply {
|
||||
// Debug metadata
|
||||
add("/META-INF/*.version")
|
||||
add("/META-INF/*.kotlin_module")
|
||||
add("/kotlin-tooling-metadata.json")
|
||||
|
||||
// Kotlin debugging (https://github.com/Kotlin/kotlinx.coroutines/issues/2274)
|
||||
add("/DebugProbesKt.bin")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_17
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xexplicit-backing-fields",
|
||||
"-Xcontext-parameters",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
all {
|
||||
// ReVanced Library has a dependency which conflicts with whatever this is. We don't use protobuf, so it should be fine.
|
||||
exclude(group = "com.google.api.grpc", module = "proto-google-common-protos")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 1.26.0-dev.21
|
||||
version = 1.26.0-dev.24
|
||||
|
||||
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
@@ -11,4 +11,7 @@
|
||||
-dontwarn com.google.j2objc.annotations.*
|
||||
-dontwarn java.awt.**
|
||||
-dontwarn javax.**
|
||||
-dontwarn org.slf4j.**
|
||||
-dontwarn org.slf4j.**
|
||||
|
||||
-dontwarn org.immutables.value.Generated
|
||||
-dontwarn reactor.blockhound.integration.BlockHoundIntegration
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package app.revanced.manager
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
@@ -25,6 +22,8 @@ import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.toRoute
|
||||
import app.revanced.manager.ui.model.navigation.Announcement
|
||||
import app.revanced.manager.ui.model.navigation.Announcements
|
||||
import app.revanced.manager.ui.model.navigation.AppSelector
|
||||
import app.revanced.manager.ui.model.navigation.ComplexParameter
|
||||
import app.revanced.manager.ui.model.navigation.Dashboard
|
||||
@@ -33,6 +32,8 @@ import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
import app.revanced.manager.ui.model.navigation.Settings
|
||||
import app.revanced.manager.ui.model.navigation.Update
|
||||
import app.revanced.manager.ui.screen.AnnouncementScreen
|
||||
import app.revanced.manager.ui.screen.AnnouncementsScreen
|
||||
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||
import app.revanced.manager.ui.screen.DashboardScreen
|
||||
import app.revanced.manager.ui.screen.InstalledAppInfoScreen
|
||||
@@ -62,6 +63,7 @@ import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@ExperimentalAnimationApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -74,21 +76,10 @@ class MainActivity : AppCompatActivity() {
|
||||
val vm: MainViewModel = getActivityViewModel()
|
||||
|
||||
setContent {
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = vm::applyLegacySettings
|
||||
)
|
||||
val theme by vm.prefs.theme.getAsState()
|
||||
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
||||
val pureBlackTheme by vm.prefs.pureBlackTheme.getAsState()
|
||||
|
||||
EventEffect(vm.legacyImportActivityFlow) {
|
||||
try {
|
||||
launcher.launch(it)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
}
|
||||
}
|
||||
|
||||
ReVancedManagerTheme(
|
||||
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
||||
dynamicColor = dynamicColor,
|
||||
@@ -117,7 +108,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
|
||||
exitTransition = { slideOutHorizontally(targetOffsetX = { -it / 3 }) },
|
||||
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it / 3 }) },
|
||||
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) },
|
||||
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
|
||||
) {
|
||||
composable<Dashboard> {
|
||||
DashboardScreen(
|
||||
@@ -133,6 +124,12 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
},
|
||||
onAppClick = { packageName ->
|
||||
navController.navigate(InstalledApplicationInfo(packageName))
|
||||
},
|
||||
onAnnouncementsClick = {
|
||||
navController.navigate(Announcements)
|
||||
},
|
||||
onAnnouncementClick = { announcement ->
|
||||
navController.navigateComplex(Announcement, announcement)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -178,6 +175,22 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
)
|
||||
}
|
||||
|
||||
composable<Announcements> {
|
||||
AnnouncementsScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onAnnouncementClick = { announcement ->
|
||||
navController.navigateComplex(Announcement, announcement)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable<Announcement> {
|
||||
AnnouncementScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
announcement = it.getComplexArg()
|
||||
)
|
||||
}
|
||||
|
||||
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
|
||||
composable<SelectedApplicationInfo.Main> {
|
||||
val parentBackStackEntry = navController.navGraphEntry(it)
|
||||
|
||||
@@ -5,8 +5,6 @@ import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import app.revanced.manager.patcher.patch.Option
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
@@ -36,7 +34,7 @@ import kotlin.reflect.typeOf
|
||||
data class Option(
|
||||
@ColumnInfo(name = "group") val group: Int,
|
||||
@ColumnInfo(name = "patch_name") val patchName: String,
|
||||
@ColumnInfo(name = "key") val key: String,
|
||||
@ColumnInfo(name = "key") val name: String,
|
||||
// Encoded as Json.
|
||||
@ColumnInfo(name = "value") val value: SerializedValue,
|
||||
) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.koin.dsl.module
|
||||
|
||||
val repositoryModule = module {
|
||||
singleOf(::ReVancedAPI)
|
||||
singleOf(::AnnouncementRepository)
|
||||
singleOf(::Filesystem) {
|
||||
createdAtStart()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ val viewModelModule = module {
|
||||
viewModelOf(::AppSelectorViewModel)
|
||||
viewModelOf(::PatcherViewModel)
|
||||
viewModelOf(::UpdateViewModel)
|
||||
viewModelOf(::AnnouncementsViewModel)
|
||||
viewModelOf(::ChangelogsViewModel)
|
||||
viewModelOf(::ImportExportViewModel)
|
||||
viewModelOf(::AboutViewModel)
|
||||
|
||||
@@ -121,7 +121,7 @@ class RootInstaller(
|
||||
execute("pm install \"${stockApp.absolutePath}\"").assertSuccess("Failed to install stock app")
|
||||
}
|
||||
|
||||
remoteFS.getFile(modulePath).mkdir()
|
||||
remoteFS.getFile(modulePath).mkdirs()
|
||||
|
||||
listOf(
|
||||
"service.sh",
|
||||
|
||||
@@ -20,6 +20,8 @@ class PreferencesManager(
|
||||
val keystoreAlias = stringPreference("keystore_alias", KeystoreManager.DEFAULT)
|
||||
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
||||
|
||||
val readAnnouncements = longSetPreference("read_announcements", emptySet())
|
||||
val selectedAnnouncementTags = stringSetPreference("selected_announcement_tags", setOf("revanced", "manager"))
|
||||
val firstLaunch = booleanPreference("first_launch", true)
|
||||
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
|
||||
val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true)
|
||||
|
||||
@@ -29,6 +29,9 @@ abstract class BasePreferencesManager(private val context: Context, name: String
|
||||
protected fun stringSetPreference(key: String, default: Set<String>) =
|
||||
StringSetPreference(dataStore, key, default)
|
||||
|
||||
protected fun longSetPreference(key: String, default: Set<Long>) =
|
||||
LongSetPreference(dataStore, key, default)
|
||||
|
||||
protected fun booleanPreference(key: String, default: Boolean) =
|
||||
BooleanPreference(dataStore, key, default)
|
||||
|
||||
@@ -56,9 +59,13 @@ class EditorContext(private val prefs: MutablePreferences) {
|
||||
get() = prefs.run { read() }
|
||||
set(value) = prefs.run { write(value) }
|
||||
|
||||
operator fun Preference<Set<String>>.plusAssign(value: String) = prefs.run {
|
||||
operator fun <T> Preference<Set<T>>.plusAssign(value: T) = prefs.run {
|
||||
write(read() + value)
|
||||
}
|
||||
|
||||
operator fun <T> Preference<Set<T>>.minusAssign(value: T) = prefs.run {
|
||||
write(read() subtract setOf(value))
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Preference<T>(
|
||||
@@ -125,6 +132,19 @@ class StringSetPreference(
|
||||
override val key = stringSetPreferencesKey(key)
|
||||
}
|
||||
|
||||
class LongSetPreference(
|
||||
dataStore: DataStore<Preferences>,
|
||||
key: String,
|
||||
default: Set<Long>
|
||||
) : Preference<Set<Long>>(dataStore, default) {
|
||||
private val key = stringSetPreferencesKey(key)
|
||||
|
||||
override fun Preferences.read() = this[key]?.mapTo(mutableSetOf()) { it.toLong() } ?: default
|
||||
override fun MutablePreferences.write(value: Set<Long>) {
|
||||
this[key] = value.mapTo(mutableSetOf()) { it.toString() }
|
||||
}
|
||||
}
|
||||
|
||||
class BooleanPreference(
|
||||
dataStore: DataStore<Preferences>,
|
||||
key: String,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package app.revanced.manager.domain.repository
|
||||
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncement
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncementTag
|
||||
import app.revanced.manager.network.utils.getOrNull
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class AnnouncementRepository(
|
||||
private val api: ReVancedAPI
|
||||
) {
|
||||
private val mutex = Mutex()
|
||||
private var cachedAnnouncements: List<ReVancedAnnouncement>? = null
|
||||
private var cachedTags: List<ReVancedAnnouncementTag>? = null
|
||||
|
||||
suspend fun getAnnouncements(forceRefresh: Boolean = false): List<ReVancedAnnouncement>? {
|
||||
mutex.withLock {
|
||||
if (cachedAnnouncements == null || forceRefresh) {
|
||||
cachedAnnouncements = api.getAnnouncements().getOrNull()
|
||||
}
|
||||
return cachedAnnouncements
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTags(forceRefresh: Boolean = false): List<ReVancedAnnouncementTag>? {
|
||||
mutex.withLock {
|
||||
if (cachedTags == null || forceRefresh) {
|
||||
cachedTags = api.getAnnouncementTags().getOrNull()
|
||||
}
|
||||
return cachedTags
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.InputStream
|
||||
import kotlin.collections.joinToString
|
||||
@@ -115,32 +116,18 @@ class PatchBundleRepository(
|
||||
Log.d(tag, "Bundle: $it")
|
||||
}
|
||||
|
||||
val sources = entities.associate { it.uid to it.load() }.toPersistentMap()
|
||||
val sources = entities
|
||||
.associateTo(mutableMapOf()) { it.uid to it.load() }
|
||||
sources.forEach syncName@{ (uid, src) ->
|
||||
val newName = src.patchBundle?.manifestAttributes?.name?.takeIf { it != src.name }
|
||||
?: return@syncName
|
||||
|
||||
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())
|
||||
updateDb(uid) { it.copy(name = newName) }
|
||||
sources[uid] = src.copy(name = newName)
|
||||
}
|
||||
val info = loadMetadata(sources).toPersistentMap()
|
||||
|
||||
return State(sources, info)
|
||||
val info = loadMetadata(sources).toPersistentMap()
|
||||
return State(sources.toPersistentMap(), info)
|
||||
}
|
||||
|
||||
suspend fun reload() = dispatchAction("Full reload") {
|
||||
@@ -157,38 +144,46 @@ class PatchBundleRepository(
|
||||
return all
|
||||
}
|
||||
|
||||
private suspend fun loadMetadata(sources: Map<Int, PatchBundleSource>): Map<Int, PatchBundleInfo.Global> {
|
||||
private suspend fun loadMetadata(sources: MutableMap<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)
|
||||
runInterruptible(Dispatchers.Default) {
|
||||
PatchBundle.Loader.metadata(map.keys)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} 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
|
||||
}
|
||||
})
|
||||
sources.entries.forEach { entry ->
|
||||
entry.setValue(entry.value.copy(error = error))
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
val output = buildMap {
|
||||
metadata.forEach { (bundle, result) ->
|
||||
val src = map[bundle]!!
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
sources[src.uid] = src.copy(error = error)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
this[src.uid] = PatchBundleInfo.Global(
|
||||
src.name,
|
||||
bundle.manifestAttributes?.version,
|
||||
src.uid,
|
||||
result.getOrThrow().toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
suspend fun isVersionAllowed(packageName: String, version: String) =
|
||||
@@ -313,9 +308,9 @@ class PatchBundleRepository(
|
||||
}
|
||||
|
||||
suspend fun reloadApiBundles() = dispatchAction("Reload API bundles") {
|
||||
this@PatchBundleRepository.sources.first().filterIsInstance<APIPatchBundle>().forEach {
|
||||
with(it) { deleteLocalFile() }
|
||||
updateDb(it.uid) { it.copy(versionHash = null) }
|
||||
this@PatchBundleRepository.sources.first().filterIsInstance<APIPatchBundle>().forEach { src ->
|
||||
with(src) { deleteLocalFile() }
|
||||
updateDb(src.uid) { it.copy(versionHash = null) }
|
||||
}
|
||||
|
||||
doReload()
|
||||
|
||||
@@ -35,15 +35,15 @@ class PatchOptionsRepository(db: AppDatabase) {
|
||||
bundlePatchOptions.getOrPut(dbOption.patchName, ::mutableMapOf)
|
||||
|
||||
val option =
|
||||
bundlePatches[sourceUid]?.get(dbOption.patchName)?.options?.find { it.key == dbOption.key }
|
||||
bundlePatches[sourceUid]?.get(dbOption.patchName)?.options?.find { it.name == dbOption.name }
|
||||
if (option != null) {
|
||||
try {
|
||||
deserializedPatchOptions[option.key] =
|
||||
deserializedPatchOptions[option.name] =
|
||||
dbOption.value.deserializeFor(option)
|
||||
} catch (e: Option.SerializationException) {
|
||||
Log.w(
|
||||
tag,
|
||||
"Option ${dbOption.patchName}:${option.key} could not be deserialized",
|
||||
"Option ${dbOption.patchName}:${option.name} could not be deserialized",
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package app.revanced.manager.network.api
|
||||
|
||||
import app.revanced.manager.BuildConfig
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncement
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncementTag
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.network.dto.ReVancedGitRepository
|
||||
import app.revanced.manager.network.dto.ReVancedInfo
|
||||
@@ -29,6 +31,10 @@ class ReVancedAPI(
|
||||
|
||||
private suspend inline fun <reified T> request(route: String) = request<T>(apiUrl(), route)
|
||||
|
||||
suspend fun getAnnouncements() = request<List<ReVancedAnnouncement>>("announcements")
|
||||
|
||||
suspend fun getAnnouncementTags() = request<List<ReVancedAnnouncementTag>>("announcements/tags")
|
||||
|
||||
suspend fun getAppUpdate() =
|
||||
getLatestAppInfo().getOrThrow().takeIf { it.version.removePrefix("v") != BuildConfig.VERSION_NAME }
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.revanced.manager.network.dto
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data class ReVancedAnnouncement(
|
||||
val id: Long,
|
||||
val author: String,
|
||||
val title: String,
|
||||
val content: String,
|
||||
val tags: List<String>,
|
||||
val attachments: List<String>,
|
||||
@SerialName("created_at")
|
||||
val createdAt: LocalDateTime,
|
||||
@SerialName("archived_at")
|
||||
val archivedAt: LocalDateTime,
|
||||
val level: Int,
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,8 @@
|
||||
package app.revanced.manager.network.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ReVancedAnnouncementTag(
|
||||
val name: String
|
||||
)
|
||||
@@ -4,66 +4,71 @@ import app.revanced.library.ApkUtils.applyTo
|
||||
import app.revanced.manager.patcher.Session.Companion.component1
|
||||
import app.revanced.manager.patcher.Session.Companion.component2
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherConfig
|
||||
import app.revanced.patcher.PatchesResult
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import app.revanced.patcher.patcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
|
||||
internal typealias PatchList = List<Patch<*>>
|
||||
internal typealias PatchList = List<Patch>
|
||||
private typealias Patcher = (emit: (PatchResult) -> Unit) -> PatchesResult
|
||||
|
||||
class Session(
|
||||
cacheDir: String,
|
||||
frameworkDir: String,
|
||||
aaptPath: String,
|
||||
private val frameworkDir: String,
|
||||
private val aaptPath: String,
|
||||
private val logger: Logger,
|
||||
private val input: File,
|
||||
private val onEvent: (ProgressEvent) -> Unit,
|
||||
) : Closeable {
|
||||
private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
|
||||
private val patcher = Patcher(
|
||||
PatcherConfig(
|
||||
apkFile = input,
|
||||
temporaryFilesPath = tempDir,
|
||||
frameworkFileDirectory = frameworkDir,
|
||||
aaptBinaryPath = aaptPath
|
||||
)
|
||||
)
|
||||
|
||||
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) {
|
||||
this().collect { (patch, exception) ->
|
||||
val index = selectedPatches.indexOf(patch)
|
||||
if (index == -1) return@collect
|
||||
|
||||
if (exception != null) {
|
||||
private suspend fun applyPatchesVerbose(patcher: Patcher, indices: Map<Patch, Int>) =
|
||||
withContext(
|
||||
Dispatchers.Default
|
||||
) {
|
||||
val context = currentCoroutineContext()
|
||||
patcher { (patch, exception) ->
|
||||
// Make the patching process cancelable.
|
||||
context.ensureActive()
|
||||
|
||||
val index = indices[patch] ?: return@patcher
|
||||
|
||||
if (exception != null) {
|
||||
onEvent(
|
||||
ProgressEvent.Failed(
|
||||
StepId.ExecutePatch(index),
|
||||
exception.toRemoteError(),
|
||||
)
|
||||
)
|
||||
logger.error("${patch.name} failed:")
|
||||
logger.error(exception.stackTraceToString())
|
||||
throw exception
|
||||
}
|
||||
|
||||
onEvent(
|
||||
ProgressEvent.Failed(
|
||||
ProgressEvent.Completed(
|
||||
StepId.ExecutePatch(index),
|
||||
exception.toRemoteError(),
|
||||
)
|
||||
)
|
||||
logger.error("${patch.name} failed:")
|
||||
logger.error(exception.stackTraceToString())
|
||||
throw exception
|
||||
|
||||
logger.info("${patch.name} succeeded")
|
||||
}
|
||||
|
||||
onEvent(
|
||||
ProgressEvent.Completed(
|
||||
StepId.ExecutePatch(index),
|
||||
)
|
||||
)
|
||||
|
||||
logger.info("${patch.name} succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun run(output: File, selectedPatches: PatchList) {
|
||||
runStep(StepId.ExecutePatches, onEvent) {
|
||||
val indices = HashMap<Patch, Int>(selectedPatches.size)
|
||||
selectedPatches.forEachIndexed { idx, patch -> indices[patch] = idx }
|
||||
|
||||
val result = runStep(StepId.ExecutePatches, onEvent) {
|
||||
java.util.logging.Logger.getLogger("").apply {
|
||||
handlers.forEach {
|
||||
it.close()
|
||||
@@ -73,18 +78,21 @@ class Session(
|
||||
addHandler(logger.handler)
|
||||
}
|
||||
|
||||
with(patcher) {
|
||||
logger.info("Merging integrations")
|
||||
this += selectedPatches.toSet()
|
||||
|
||||
logger.info("Applying patches...")
|
||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||
val patcher = patcher(
|
||||
apkFile = input,
|
||||
temporaryFilesPath = tempDir,
|
||||
frameworkFileDirectory = frameworkDir,
|
||||
aaptBinaryPath = File(aaptPath)
|
||||
) { _packageName, _version ->
|
||||
selectedPatches.toSet()
|
||||
}
|
||||
|
||||
logger.info("Applying patches...")
|
||||
applyPatchesVerbose(patcher, indices)
|
||||
}
|
||||
|
||||
runStep(StepId.WriteAPK, onEvent) {
|
||||
logger.info("Writing patched files...")
|
||||
val result = patcher.get()
|
||||
|
||||
val patched = tempDir.resolve("result.apk")
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -102,7 +110,6 @@ class Session(
|
||||
|
||||
override fun close() {
|
||||
tempDir.deleteRecursively()
|
||||
patcher.close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -2,7 +2,8 @@ package app.revanced.manager.patcher.patch
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import android.os.Parcelable
|
||||
import app.revanced.patcher.patch.loadPatchesFromDex
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.loadPatches
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@@ -54,31 +55,45 @@ data class PatchBundle(val patchesJar: String) : Parcelable {
|
||||
)
|
||||
|
||||
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 }
|
||||
private fun patches(bundles: Iterable<PatchBundle>) = buildMap {
|
||||
val bundleMap = bundles.associateBy { it.patchesJar }
|
||||
|
||||
loadPatches(
|
||||
*bundleMap.keys.map(::File).toTypedArray(),
|
||||
onFailedToLoad = { file, throwable ->
|
||||
this[bundleMap[file.absolutePath]!!] = Result.failure(throwable)
|
||||
}
|
||||
).patchesByFile.forEach { (file, patches) ->
|
||||
putIfAbsent(bundleMap[file.absolutePath]!!, Result.success(patches))
|
||||
}
|
||||
}
|
||||
|
||||
fun metadata(bundles: Iterable<PatchBundle>): Map<PatchBundle, Result<Set<PatchInfo>>> =
|
||||
patches(bundles).mapValues { (_, result) ->
|
||||
result.map { patches ->
|
||||
patches.mapTo(
|
||||
HashSet(patches.size),
|
||||
::PatchInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun metadata(bundles: Iterable<PatchBundle>) =
|
||||
patches(bundles).mapValues { (_, patches) -> patches.map(::PatchInfo) }
|
||||
fun patches(bundles: Iterable<PatchBundle>, packageName: String): Map<PatchBundle, Set<Patch>> =
|
||||
patches(bundles).mapValues { (_, result) ->
|
||||
val patches = result.getOrDefault(emptySet())
|
||||
|
||||
fun patches(bundles: Iterable<PatchBundle>, packageName: String) =
|
||||
patches(bundles).mapValues { (_, patches) ->
|
||||
patches.filter { patch ->
|
||||
patches.filterTo(HashSet(patches.size)) { patch ->
|
||||
val compatiblePackages = patch.compatiblePackages
|
||||
?: // The patch has no compatibility constraints, which means it is universal.
|
||||
return@filter true
|
||||
return@filterTo true
|
||||
|
||||
if (!compatiblePackages.any { (name, _) -> name == packageName }) {
|
||||
// Patch is not compatible with this package.
|
||||
return@filter false
|
||||
return@filterTo false
|
||||
}
|
||||
|
||||
true
|
||||
}.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ sealed class PatchBundleInfo {
|
||||
it.options.all option@{ option ->
|
||||
if (!option.required || option.default != null) return@option true
|
||||
|
||||
option.key in opts
|
||||
option.name in opts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ data class PatchInfo(
|
||||
val compatiblePackages: ImmutableList<CompatiblePackage>?,
|
||||
val options: ImmutableList<Option<*>>?
|
||||
) {
|
||||
constructor(patch: Patch<*>) : this(
|
||||
constructor(patch: Patch) : this(
|
||||
patch.name.orEmpty(),
|
||||
patch.description,
|
||||
patch.use,
|
||||
@@ -49,7 +49,7 @@ data class PatchInfo(
|
||||
* The resulting patch cannot be executed.
|
||||
* This is necessary because some functions in ReVanced Library only accept full [Patch] objects.
|
||||
*/
|
||||
fun toPatcherPatch(): Patch<*> =
|
||||
fun toPatcherPatch(): Patch =
|
||||
resourcePatch(name = name, description = description, use = include) {
|
||||
compatiblePackages?.let { pkgs ->
|
||||
compatibleWith(*pkgs.map { it.packageName to it.versions }.toTypedArray())
|
||||
@@ -65,8 +65,7 @@ data class CompatiblePackage(
|
||||
|
||||
@Immutable
|
||||
data class Option<T>(
|
||||
val title: String,
|
||||
val key: String,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val required: Boolean,
|
||||
val type: KType,
|
||||
@@ -75,8 +74,7 @@ data class Option<T>(
|
||||
val validator: (T?) -> Boolean,
|
||||
) {
|
||||
constructor(option: PatchOption<T>) : this(
|
||||
option.title ?: option.key,
|
||||
option.key,
|
||||
option.name,
|
||||
option.description.orEmpty(),
|
||||
option.required,
|
||||
option.type,
|
||||
|
||||
@@ -212,7 +212,7 @@ fun PatchItem(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = option.title,
|
||||
text = option.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
@@ -205,7 +205,7 @@ fun <T : Any> OptionItem(
|
||||
WithOptionEditor(editor, option, value, setValue, selectionWarningEnabled) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(onClick = ::clickAction),
|
||||
headlineContent = { Text(option.title) },
|
||||
headlineContent = { Text(option.name) },
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(option.description)
|
||||
@@ -250,7 +250,7 @@ private object StringOptionEditor : OptionEditor<String> {
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = scope.dismissDialog,
|
||||
title = { Text(scope.option.title) },
|
||||
title = { Text(scope.option.name) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = fieldValue,
|
||||
@@ -330,7 +330,7 @@ private abstract class NumberOptionEditor<T : Number> : OptionEditor<T> {
|
||||
|
||||
@Composable
|
||||
override fun Dialog(scope: OptionEditorScope<T>) {
|
||||
NumberDialog(scope.option.title, scope.value, scope.option.validator) {
|
||||
NumberDialog(scope.option.name, scope.value, scope.option.validator) {
|
||||
if (it == null) return@NumberDialog scope.dismissDialog()
|
||||
|
||||
scope.submitDialog(it)
|
||||
@@ -455,7 +455,7 @@ private class PresetOptionEditor<T : Any>(private val innerEditor: OptionEditor<
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
title = { Text(scope.option.title) },
|
||||
title = { Text(scope.option.name) },
|
||||
textHorizontalPadding = PaddingValues(horizontal = 0.dp),
|
||||
text = {
|
||||
val presets = remember(scope.option.presets) {
|
||||
@@ -496,8 +496,7 @@ private class PresetOptionEditor<T : Any>(private val innerEditor: OptionEditor<
|
||||
private class ListOptionEditor<T : Serializable>(private val elementEditor: OptionEditor<T>) :
|
||||
OptionEditor<List<T>> {
|
||||
private fun createElementOption(option: Option<List<T>>) = Option<T>(
|
||||
option.title,
|
||||
option.key,
|
||||
option.name,
|
||||
option.description,
|
||||
option.required,
|
||||
option.type.arguments.first().type!!,
|
||||
@@ -566,7 +565,7 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
||||
R.plurals.selected_count,
|
||||
deletionTargets.size,
|
||||
deletionTargets.size
|
||||
) else scope.option.title,
|
||||
) else scope.option.name,
|
||||
onBackClick = back,
|
||||
backIcon = {
|
||||
if (deleteMode) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.manager.ui.model.navigation
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncement
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
@@ -22,6 +23,12 @@ data class InstalledApplicationInfo(val packageName: String)
|
||||
@Serializable
|
||||
data class Update(val downloadOnScreenEntry: Boolean = false)
|
||||
|
||||
@Serializable
|
||||
data object Announcements
|
||||
|
||||
@Serializable
|
||||
data object Announcement : ComplexParameter<ReVancedAnnouncement>
|
||||
|
||||
@Serializable
|
||||
data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.ViewModelParams> {
|
||||
@Parcelize
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.MotionEvent
|
||||
import android.webkit.WebView
|
||||
import android.widget.FrameLayout
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TwoRowsTopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.children
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncement
|
||||
import app.revanced.manager.util.relativeTime
|
||||
import org.intellij.lang.annotations.Language
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun AnnouncementScreen(
|
||||
onBackClick: () -> Unit,
|
||||
announcement: ReVancedAnnouncement
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
|
||||
canScroll = {
|
||||
scrollState.canScrollBackward || scrollState.canScrollForward
|
||||
}
|
||||
)
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val linkColor = MaterialTheme.colorScheme.primary
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TwoRowsTopAppBar(
|
||||
title = { expanded ->
|
||||
Text(
|
||||
text = announcement.title,
|
||||
style = if (expanded) MaterialTheme.typography.headlineSmall else MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
subtitle = {
|
||||
val createDate = announcement.createdAt.relativeTime(LocalContext.current)
|
||||
Text("$createDate · ${announcement.author}")
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
AndroidView(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(scrollState)
|
||||
.padding(horizontal = 10.dp),
|
||||
factory = {
|
||||
val webView = WebView(it).apply {
|
||||
setBackgroundColor(0)
|
||||
isVerticalScrollBarEnabled = false
|
||||
isHorizontalScrollBarEnabled = false
|
||||
isLongClickable = false
|
||||
setOnLongClickListener { true }
|
||||
isHapticFeedbackEnabled = false
|
||||
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
|
||||
|
||||
// Disable WebView's internal scrolling
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
setOnTouchListener { _, event ->
|
||||
event.action == MotionEvent.ACTION_MOVE
|
||||
}
|
||||
}
|
||||
FrameLayout(it).apply {
|
||||
addView(webView)
|
||||
}
|
||||
},
|
||||
update = {
|
||||
val webView = it.children.first() as WebView
|
||||
@Language("HTML")
|
||||
val style = """
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body {
|
||||
color: ${textColor.toCss()};
|
||||
}
|
||||
a {
|
||||
color: ${linkColor.toCss()};
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${announcement.content}
|
||||
</body>
|
||||
</html>
|
||||
""".trimIndent()
|
||||
webView.loadData(style, "text/html", "UTF-8")
|
||||
},
|
||||
onRelease = {
|
||||
val webView = it.children.first() as WebView
|
||||
webView.destroy()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Color.toCss(): String {
|
||||
return "rgba(${red * 255f}, ${green * 255f}, ${blue * 255f}, $alpha)"
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.outlined.FilterAlt
|
||||
import androidx.compose.material.icons.outlined.Inventory2
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
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.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncement
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.viewmodel.AnnouncementsViewModel
|
||||
import app.revanced.manager.util.relativeTime
|
||||
import app.revanced.manager.util.transparentListItemColors
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import kotlin.time.Clock
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AnnouncementsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onAnnouncementClick: (ReVancedAnnouncement) -> Unit,
|
||||
vm: AnnouncementsViewModel = koinViewModel(),
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
var showFilterSheet by rememberSaveable { mutableStateOf(false) }
|
||||
val tags by vm.tags.collectAsStateWithLifecycle()
|
||||
val selectedTags by vm.selectedTags.getAsState()
|
||||
val showArchived by vm.showArchived.collectAsStateWithLifecycle()
|
||||
val announcements by vm.announcements.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
if (showFilterSheet) {
|
||||
FilterBottomSheet(
|
||||
onDismissRequest = { showFilterSheet = false },
|
||||
tags = tags.orEmpty(),
|
||||
selectedTags = selectedTags,
|
||||
showArchived = showArchived,
|
||||
onShowArchivedChange = { vm.showArchived.value = it },
|
||||
onReset = vm::resetTagSelection,
|
||||
changeSelection = vm::changeTagSelection
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = { Text(stringResource(R.string.announcements)) },
|
||||
onBackClick = onBackClick,
|
||||
actions = {
|
||||
if (tags != null) {
|
||||
IconButton(onClick = { showFilterSheet = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FilterAlt,
|
||||
contentDescription = stringResource(R.string.announcements_filter_tag)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
val readAnnouncements by vm.readAnnouncements.getAsState()
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
verticalArrangement = if (announcements.isNullOrEmpty()) Arrangement.Center else Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
announcements?.let { announcements ->
|
||||
if (announcements.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.no_announcements_found),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(
|
||||
items = announcements,
|
||||
key = { _, announcement ->
|
||||
announcement.id
|
||||
}
|
||||
) { i, announcement ->
|
||||
if (i != 0) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
AnnouncementCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
onClick = {
|
||||
vm.markAnnouncementRead(announcement.id)
|
||||
onAnnouncementClick(announcement)
|
||||
},
|
||||
title = announcement.title,
|
||||
date = announcement.createdAt.relativeTime(LocalContext.current),
|
||||
author = announcement.author,
|
||||
content = announcement.content,
|
||||
unread = announcement.id !in readAnnouncements,
|
||||
archived = announcement.archivedAt.toInstant(TimeZone.UTC) < Clock.System.now()
|
||||
)
|
||||
}
|
||||
}
|
||||
} ?: item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
LoadingIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun FilterBottomSheet(
|
||||
onDismissRequest: () -> Unit,
|
||||
tags: List<String>,
|
||||
selectedTags: Set<String>,
|
||||
showArchived: Boolean,
|
||||
onShowArchivedChange: (Boolean) -> Unit,
|
||||
onReset: () -> Unit,
|
||||
changeSelection: (String) -> Unit
|
||||
) {
|
||||
ModalBottomSheet(onDismissRequest = onDismissRequest) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.announcements_filter_tag),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
tags.forEach { tag ->
|
||||
FilterChip(
|
||||
selected = tag in selectedTags,
|
||||
onClick = {
|
||||
changeSelection(tag)
|
||||
},
|
||||
label = { Text(tag) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(onClick = { onShowArchivedChange(!showArchived) }),
|
||||
headlineContent = { Text(text = stringResource(R.string.announcements_show_archived)) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = showArchived,
|
||||
onCheckedChange = onShowArchivedChange
|
||||
)
|
||||
},
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
|
||||
TextButton(modifier = Modifier.align(Alignment.End), onClick = onReset) {
|
||||
Text(stringResource(R.string.reset))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnnouncementCard(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
title: String,
|
||||
date: String,
|
||||
author: String,
|
||||
content: String,
|
||||
unread: Boolean,
|
||||
archived: Boolean
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clickable(onClick = onClick)
|
||||
.background(if (unread) MaterialTheme.colorScheme.surfaceContainerLow else MaterialTheme.colorScheme.surface)
|
||||
.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = if (unread) FontWeight.ExtraBold else null
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "$date • $author",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = if (unread) FontWeight.ExtraBold else null
|
||||
)
|
||||
if (archived) {
|
||||
Icon(
|
||||
Icons.Outlined.Inventory2,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
}
|
||||
if (unread) {
|
||||
Badge(modifier = Modifier.size(6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
Icon(Icons.Default.ChevronRight, contentDescription = null)
|
||||
}
|
||||
// TODO add announcement summary
|
||||
// val textColor = MaterialTheme.colorScheme.onSurface
|
||||
// val linkColor = MaterialTheme.colorScheme.primary
|
||||
// AndroidView(
|
||||
// factory = {
|
||||
// WebView(it).apply {
|
||||
// setBackgroundColor(0)
|
||||
// isVerticalScrollBarEnabled = false
|
||||
// isHorizontalScrollBarEnabled = false
|
||||
// isLongClickable = false
|
||||
// setOnLongClickListener { true }
|
||||
// isHapticFeedbackEnabled = false
|
||||
//
|
||||
// // Disable WebView's internal scrolling
|
||||
// @SuppressLint("ClickableViewAccessibility")
|
||||
// setOnTouchListener { _, event ->
|
||||
// event.action == MotionEvent.ACTION_MOVE
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// update = {
|
||||
// @Language("HTML")
|
||||
// val body = """
|
||||
// <html>
|
||||
// <head>
|
||||
// <meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
// <style>
|
||||
// * {
|
||||
// font-size: 12px;
|
||||
// font-weight: normal;
|
||||
// }
|
||||
// body {
|
||||
// margin: 0;
|
||||
// padding: 0;
|
||||
// color: ${textColor.toCss()};
|
||||
// overflow: hidden;
|
||||
// display: -webkit-box;
|
||||
// -webkit-box-orient: vertical;
|
||||
// -webkit-line-clamp: 3;
|
||||
// text-overflow: ellipsis;
|
||||
// }
|
||||
// a {
|
||||
// color: ${linkColor.toCss()};
|
||||
// }
|
||||
// </style>
|
||||
// </head>
|
||||
// <body>
|
||||
// $content
|
||||
// </body>
|
||||
// </html>
|
||||
// """.trimIndent()
|
||||
//
|
||||
// it.loadData(body, "text/html", "UTF-8")
|
||||
// },
|
||||
// onReset = {},
|
||||
// onRelease = { it.destroy() }
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
//private fun Color.toCss(): String {
|
||||
// return "rgba(${red * 255f}, ${green * 255f}, ${blue * 255f}, $alpha)"
|
||||
//}
|
||||
@@ -19,11 +19,13 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.BatteryAlert
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.outlined.Apps
|
||||
import androidx.compose.material.icons.outlined.BugReport
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Notifications
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.Source
|
||||
@@ -58,6 +60,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncement
|
||||
import app.revanced.manager.patcher.aapt.Aapt
|
||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
@@ -91,6 +94,8 @@ fun DashboardScreen(
|
||||
onAppSelectorClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onUpdateClick: () -> Unit,
|
||||
onAnnouncementsClick: () -> Unit,
|
||||
onAnnouncementClick: (ReVancedAnnouncement) -> Unit,
|
||||
onDownloaderClick: () -> Unit,
|
||||
onAppClick: (String) -> Unit
|
||||
) {
|
||||
@@ -130,17 +135,16 @@ fun DashboardScreen(
|
||||
)
|
||||
}
|
||||
|
||||
var showUpdateDialog by rememberSaveable { mutableStateOf(vm.prefs.showManagerUpdateDialogOnLaunch.getBlocking()) }
|
||||
val availableUpdate by remember {
|
||||
derivedStateOf { vm.updatedManagerVersion.takeIf { showUpdateDialog } }
|
||||
}
|
||||
var showUpdateDialog by rememberSaveable { mutableStateOf(true) }
|
||||
val showManagerUpdateDialogOnLaunch by vm.prefs.showManagerUpdateDialogOnLaunch.getAsState()
|
||||
val availableUpdate = vm.updatedManagerVersion
|
||||
|
||||
availableUpdate?.let { version ->
|
||||
if (showUpdateDialog && showManagerUpdateDialogOnLaunch && availableUpdate != null) {
|
||||
AvailableUpdateDialog(
|
||||
onDismiss = { showUpdateDialog = false },
|
||||
setShowManagerUpdateDialogOnLaunch = vm::setShowManagerUpdateDialogOnLaunch,
|
||||
onConfirm = onUpdateClick,
|
||||
newVersion = version
|
||||
newVersion = availableUpdate
|
||||
)
|
||||
}
|
||||
|
||||
@@ -220,6 +224,20 @@ fun DashboardScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onAnnouncementsClick) {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (vm.unreadAnnouncement != null) {
|
||||
Badge(modifier = Modifier.size(6.dp))
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Notifications,
|
||||
stringResource(R.string.announcements)
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onSettingsClick) {
|
||||
Icon(Icons.Outlined.Settings, stringResource(R.string.settings))
|
||||
}
|
||||
@@ -322,7 +340,29 @@ fun DashboardScreen(
|
||||
}
|
||||
)
|
||||
}
|
||||
} else null
|
||||
} else null,
|
||||
vm.unreadAnnouncement?.let { announcement ->
|
||||
{
|
||||
NotificationCard(
|
||||
text = stringResource(R.string.new_announcement, announcement.title),
|
||||
icon = Icons.Filled.Notifications,
|
||||
actions = {
|
||||
TextButton(onClick = vm::markUnreadAnnouncementRead) {
|
||||
Text(stringResource(R.string.dismiss))
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
vm.markUnreadAnnouncementRead()
|
||||
onAnnouncementClick(announcement)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.view_announcement))
|
||||
}
|
||||
},
|
||||
isWarning = announcement.level > 0
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
HorizontalPager(
|
||||
|
||||
@@ -640,17 +640,17 @@ private fun OptionsDialog(
|
||||
) {
|
||||
if (patch.options == null) return@LazyColumnWithScrollbar
|
||||
|
||||
items(patch.options, key = { it.key }) { option ->
|
||||
val key = option.key
|
||||
items(patch.options, key = { it.name }) { option ->
|
||||
val name = option.name
|
||||
val value =
|
||||
if (values == null || !values.contains(key)) option.default else values[key]
|
||||
if (values == null || !values.contains(name)) option.default else values[name]
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
OptionItem(
|
||||
option = option as Option<Any>,
|
||||
value = value,
|
||||
setValue = {
|
||||
set(key, it)
|
||||
set(name, it)
|
||||
},
|
||||
selectionWarningEnabled = selectionWarningEnabled
|
||||
)
|
||||
|
||||
@@ -144,16 +144,16 @@ fun RequiredOptionsScreen(
|
||||
|
||||
val values = vm.getOptions(bundle.uid, it)
|
||||
it.options?.forEach { option ->
|
||||
val key = option.key
|
||||
val name = option.name
|
||||
val value =
|
||||
if (values == null || key !in values) option.default else values[key]
|
||||
if (values == null || name !in values) option.default else values[name]
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
OptionItem(
|
||||
option = option as Option<Any>,
|
||||
value = value,
|
||||
setValue = { new ->
|
||||
vm.setOption(bundle.uid, it, key, new)
|
||||
vm.setOption(bundle.uid, it, name, new)
|
||||
},
|
||||
selectionWarningEnabled = vm.selectionWarningEnabled
|
||||
)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.AnnouncementRepository
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncement
|
||||
import kotlin.time.Clock
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
|
||||
class AnnouncementsViewModel(
|
||||
private val announcementRepository: AnnouncementRepository,
|
||||
private val network: NetworkInfo,
|
||||
private val preferences: PreferencesManager
|
||||
) : ViewModel() {
|
||||
private val allAnnouncements = MutableStateFlow<List<ReVancedAnnouncement>?>(null)
|
||||
|
||||
private val _tags = MutableStateFlow<List<String>?>(null)
|
||||
val tags get() = _tags.asStateFlow()
|
||||
val selectedTags = preferences.selectedAnnouncementTags
|
||||
val readAnnouncements = preferences.readAnnouncements
|
||||
val showArchived = MutableStateFlow(false)
|
||||
|
||||
val announcements = combine(
|
||||
allAnnouncements,
|
||||
_tags,
|
||||
selectedTags.flow,
|
||||
showArchived
|
||||
) { source, tags, selectedTags, showArchived ->
|
||||
if (source == null) return@combine null
|
||||
// Only filter by tags that actually exist
|
||||
val availableTags = tags.orEmpty().toSet()
|
||||
val validSelected = selectedTags.intersect(availableTags)
|
||||
|
||||
val visibleAnnouncements = if (showArchived) {
|
||||
source
|
||||
} else {
|
||||
source.filter { announcement ->
|
||||
announcement.archivedAt.toInstant(TimeZone.UTC) > Clock.System.now()
|
||||
}
|
||||
}
|
||||
|
||||
if (validSelected.isEmpty()) {
|
||||
visibleAnnouncements
|
||||
} else {
|
||||
visibleAnnouncements.filter { announcement ->
|
||||
announcement.tags.any(validSelected::contains)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
loadData()
|
||||
}
|
||||
|
||||
fun markAnnouncementRead(id: Long) {
|
||||
viewModelScope.launch {
|
||||
preferences.edit {
|
||||
preferences.readAnnouncements += id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun changeTagSelection(tag: String) = viewModelScope.launch {
|
||||
preferences.edit {
|
||||
if (tag in selectedTags.value) selectedTags -= tag
|
||||
else selectedTags += tag
|
||||
}
|
||||
}
|
||||
|
||||
fun resetTagSelection() = viewModelScope.launch {
|
||||
selectedTags.update(preferences.selectedAnnouncementTags.default)
|
||||
}
|
||||
|
||||
private fun loadData() {
|
||||
viewModelScope.launch {
|
||||
if (!network.isConnected()) {
|
||||
allAnnouncements.value = emptyList()
|
||||
_tags.value = emptyList()
|
||||
return@launch
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
announcementRepository.getAnnouncements()?.let {
|
||||
allAnnouncements.value = it
|
||||
}
|
||||
announcementRepository.getTags()?.let {
|
||||
_tags.value = it.map { tag -> tag.name }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,21 +16,29 @@ import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.AnnouncementRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderRepository
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncement
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlin.time.Clock
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DashboardViewModel(
|
||||
private val app: Application,
|
||||
private val patchBundleRepository: PatchBundleRepository,
|
||||
private val downloaderRepository: DownloaderRepository,
|
||||
private val announcementRepository: AnnouncementRepository,
|
||||
private val reVancedAPI: ReVancedAPI,
|
||||
private val networkInfo: NetworkInfo,
|
||||
val prefs: PreferencesManager,
|
||||
@@ -57,12 +65,16 @@ class DashboardViewModel(
|
||||
var showBatteryOptimizationsWarning by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var unreadAnnouncement by mutableStateOf<ReVancedAnnouncement?>(null)
|
||||
private set
|
||||
|
||||
private val bundleListEventsChannel = Channel<BundleListViewModel.Event>()
|
||||
val bundleListEventsFlow = bundleListEventsChannel.receiveAsFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
checkForManagerUpdates()
|
||||
checkForAnnouncements()
|
||||
updateBatteryOptimizationsWarning()
|
||||
}
|
||||
}
|
||||
@@ -79,6 +91,44 @@ class DashboardViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkForAnnouncements() {
|
||||
uiSafe(app, R.string.failed_to_check_updates, "Failed to check for announcements") {
|
||||
val announcements = withContext(Dispatchers.IO) {
|
||||
announcementRepository.getAnnouncements()
|
||||
} ?: throw IllegalStateException("Announcements could not be retrieved")
|
||||
|
||||
val readAnnouncements = prefs.readAnnouncements.get()
|
||||
if (readAnnouncements.isEmpty()) {
|
||||
val announcementIds = announcements.mapTo(mutableSetOf()) { it.id }
|
||||
prefs.readAnnouncements.update(announcementIds)
|
||||
return@uiSafe
|
||||
}
|
||||
|
||||
unreadAnnouncement = announcements.firstOrNull { announcement ->
|
||||
val isNotArchived =
|
||||
announcement.archivedAt.toInstant(TimeZone.UTC) > Clock.System.now()
|
||||
|
||||
val hasRelevantTag = "revanced" in announcement.tags ||
|
||||
"manager" in announcement.tags
|
||||
|
||||
val isUnread = announcement.id !in readAnnouncements
|
||||
|
||||
isNotArchived && hasRelevantTag && isUnread
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun markUnreadAnnouncementRead() {
|
||||
viewModelScope.launch {
|
||||
unreadAnnouncement?.let {
|
||||
prefs.edit {
|
||||
prefs.readAnnouncements += it.id
|
||||
}
|
||||
}
|
||||
unreadAnnouncement = null
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBatteryOptimizationsWarning() {
|
||||
showBatteryOptimizationsWarning =
|
||||
!powerManager.isIgnoringBatteryOptimizations(app.packageName)
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.util.Base64
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||
import app.revanced.manager.domain.manager.KeystoreManager
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||
import app.revanced.manager.domain.repository.SerializedSelection
|
||||
@@ -27,11 +28,16 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
private const val LEGACY_LIST_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu!"
|
||||
|
||||
class MainViewModel(
|
||||
private val patchBundleRepository: PatchBundleRepository,
|
||||
private val patchSelectionRepository: PatchSelectionRepository,
|
||||
private val downloadedAppRepository: DownloadedAppRepository,
|
||||
private val installedAppRepository: InstalledAppRepository,
|
||||
private val keystoreManager: KeystoreManager,
|
||||
private val app: Application,
|
||||
val prefs: PreferencesManager,
|
||||
@@ -39,8 +45,6 @@ class MainViewModel(
|
||||
) : ViewModel() {
|
||||
private val appSelectChannel = Channel<SelectedApp>()
|
||||
val appSelectFlow = appSelectChannel.receiveAsFlow()
|
||||
private val legacyImportActivityChannel = Channel<Intent>()
|
||||
val legacyImportActivityFlow = legacyImportActivityChannel.receiveAsFlow()
|
||||
|
||||
private suspend fun suggestedVersion(packageName: String) =
|
||||
patchBundleRepository.suggestedVersions.first()[packageName]
|
||||
@@ -72,43 +76,55 @@ class MainViewModel(
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
if (!prefs.firstLaunch.get()) return@launch
|
||||
legacyImportActivityChannel.send(Intent().apply {
|
||||
setClassName(
|
||||
"app.revanced.manager.flutter",
|
||||
"app.revanced.manager.flutter.ExportSettingsActivity"
|
||||
)
|
||||
})
|
||||
val flutterPrefs = app.getSharedPreferences("FlutterSharedPreferences", MODE_PRIVATE)
|
||||
if (flutterPrefs.all.isNotEmpty()) applyLegacySettings(flutterPrefs)
|
||||
}
|
||||
}
|
||||
|
||||
fun applyLegacySettings(result: ActivityResult) {
|
||||
if (result.resultCode != Activity.RESULT_OK) {
|
||||
app.toast(app.getString(R.string.legacy_import_failed))
|
||||
Log.e(
|
||||
tag,
|
||||
"Got unknown result code while importing legacy settings: ${result.resultCode}"
|
||||
)
|
||||
return
|
||||
fun applyLegacySettings(flutterPrefs: SharedPreferences) {
|
||||
Log.d(tag, "Migrating flutter preferences")
|
||||
val data = JSONObject().apply {
|
||||
put("keystorePassword", "s3cur3p@ssw0rd")
|
||||
|
||||
val allEntries: Map<String, *> = flutterPrefs.all
|
||||
for ((key, value) in allEntries) {
|
||||
put(key.replace("flutter.", ""), value)
|
||||
}
|
||||
}
|
||||
|
||||
val jsonStr = result.data?.getStringExtra("data")
|
||||
if (jsonStr == null) {
|
||||
app.toast(app.getString(R.string.legacy_import_failed))
|
||||
Log.e(tag, "Legacy settings data is null")
|
||||
return
|
||||
val storedPatchesFile = File(app.filesDir.parentFile.absolutePath, "/app_flutter/selected-patches.json")
|
||||
val patches: SerializedSelection? =
|
||||
if (storedPatchesFile.exists()) {
|
||||
json.decodeFromString<SerializedSelection>(storedPatchesFile.readText())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val keystoreFile = File(app.getExternalFilesDir(null), "/revanced-manager.keystore")
|
||||
val keystore: ByteArray? = if (keystoreFile.exists()) {
|
||||
val bytes = keystoreFile.readBytes()
|
||||
|
||||
keystoreFile.delete()
|
||||
|
||||
bytes
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
flutterPrefs.edit(commit = true) { clear() }
|
||||
|
||||
val settings = try {
|
||||
json.decodeFromString<LegacySettings>(jsonStr)
|
||||
json.decodeFromString<LegacySettings>(data.toString())
|
||||
} catch (e: SerializationException) {
|
||||
app.toast(app.getString(R.string.legacy_import_failed))
|
||||
Log.e(tag, "Legacy settings data could not be deserialized", e)
|
||||
return
|
||||
}
|
||||
|
||||
applyLegacySettings(settings)
|
||||
applyLegacySettings(settings, patches, keystore)
|
||||
}
|
||||
|
||||
private fun applyLegacySettings(settings: LegacySettings) = viewModelScope.launch {
|
||||
private fun applyLegacySettings(settings: LegacySettings, patches: SerializedSelection?, keystore: ByteArray?) = viewModelScope.launch {
|
||||
settings.themeMode?.let { theme ->
|
||||
val themeMap = mapOf(
|
||||
0 to Theme.SYSTEM,
|
||||
@@ -144,20 +160,39 @@ class MainViewModel(
|
||||
settings.patchesChangeEnabled?.let { disableSelectionWarning ->
|
||||
prefs.disableSelectionWarning.update(disableSelectionWarning)
|
||||
}
|
||||
settings.keystore?.let { keystore ->
|
||||
val keystoreBytes = Base64.decode(keystore, Base64.DEFAULT)
|
||||
keystore?.let { keystoreBytes ->
|
||||
keystoreManager.import(
|
||||
"alias",
|
||||
settings.keystorePassword,
|
||||
keystoreBytes.inputStream()
|
||||
)
|
||||
}
|
||||
settings.patches?.let { selection ->
|
||||
patches?.let { selection ->
|
||||
patchSelectionRepository.import(0, selection)
|
||||
}
|
||||
settings.patchedApps?.let { apps ->
|
||||
json.decodeFromString<List<String>>(apps.removePrefix(LEGACY_LIST_PREFIX)).forEach { appJson ->
|
||||
val patchedApp = json.decodeFromString<LegacyPatchedApp>(appJson)
|
||||
installedAppRepository.addOrUpdate(
|
||||
patchedApp.packageName,
|
||||
patchedApp.packageName,
|
||||
patchedApp.version,
|
||||
if (patchedApp.isRooted) InstallType.MOUNT else InstallType.DEFAULT,
|
||||
mapOf(0 to patchedApp.appliedPatches.toSet())
|
||||
)
|
||||
}
|
||||
}
|
||||
Log.d(tag, "Imported legacy settings")
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class LegacyPatchedApp(
|
||||
val packageName: String,
|
||||
val version: String,
|
||||
val isRooted: Boolean,
|
||||
val appliedPatches: List<String>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class LegacySettings(
|
||||
val keystorePassword: String,
|
||||
@@ -168,7 +203,6 @@ class MainViewModel(
|
||||
val experimentalPatchesEnabled: Boolean? = null,
|
||||
val patchesAutoUpdate: Boolean? = null,
|
||||
val patchesChangeEnabled: Boolean? = null,
|
||||
val keystore: String? = null,
|
||||
val patches: SerializedSelection? = null,
|
||||
val patchedApps: String? = null,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
isSelected(
|
||||
bundle.uid,
|
||||
patch
|
||||
) && patch.options?.any { it.required && it.default == null && it.key !in opts } ?: false
|
||||
) && patch.options?.any { it.required && it.default == null && it.name !in opts } ?: false
|
||||
}.toList()
|
||||
}.filter { (_, patches) -> patches.isNotEmpty() }
|
||||
}
|
||||
@@ -180,13 +180,13 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
|
||||
fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
|
||||
|
||||
fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) {
|
||||
fun setOption(bundle: Int, patch: PatchInfo, name: String, value: Any?) {
|
||||
// All patches
|
||||
val patchesToOpts = patchOptions.getOrElse(bundle, ::persistentMapOf)
|
||||
// The key-value options of an individual patch
|
||||
val patchToOpts = patchesToOpts
|
||||
.getOrElse(patch.name, ::persistentMapOf)
|
||||
.put(key, value)
|
||||
.put(name, value)
|
||||
|
||||
patchOptions[bundle] = patchesToOpts.put(patch.name, patchToOpts)
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ class SelectedAppInfoViewModel(
|
||||
bundleOptions.forEach patch@{ (patchName, values) ->
|
||||
// Get all valid option keys for the patch.
|
||||
val validOptionKeys =
|
||||
patches[patchName]?.options?.map { it.key }?.toSet() ?: return@patch
|
||||
patches[patchName]?.options?.map { it.name }?.toSet() ?: return@patch
|
||||
|
||||
this@bundleOptions[patchName] = values.filterKeys { key ->
|
||||
key in validOptionKeys
|
||||
|
||||
@@ -14,7 +14,7 @@ Second \"item\" text"</string>
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">ReVanced Manager</string>
|
||||
<string name="patcher">Patcher test</string>
|
||||
<string name="patcher">Patcher</string>
|
||||
<string name="patches">Patches</string>
|
||||
<string name="cli">CLI</string>
|
||||
<string name="manager">Manager</string>
|
||||
@@ -82,6 +82,9 @@ Second \"item\" text"</string>
|
||||
<string name="auto_updates_dialog_patches">ReVanced Patches</string>
|
||||
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
|
||||
|
||||
<string name="announcements_filter_tag">Filter by tag</string>
|
||||
<string name="announcements_show_archived">Show archived</string>
|
||||
|
||||
<string name="general">General</string>
|
||||
<string name="general_description">Language, theme, dynamic color</string>
|
||||
<string name="updates">Updates</string>
|
||||
@@ -110,24 +113,24 @@ Second \"item\" text"</string>
|
||||
<string name="safeguards">Safeguards</string>
|
||||
<string name="patch_compat_check">Disable version compatibility check</string>
|
||||
<string name="patch_compat_check_description">Do not restrict patches to compatible app versions</string>
|
||||
<string name="patch_compat_check_confirmation">"Selecting incompatible patches can result in a broken app.
|
||||
<string name="patch_compat_check_confirmation">Selecting incompatible patches can result in a broken app.
|
||||
|
||||
Do you want to proceed anyways?"</string>
|
||||
Do you want to proceed anyways?</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_confirmation">"Selecting an app that is not the suggested version may cause unexpected issues.
|
||||
<string name="suggested_version_safeguard_confirmation">Selecting an app that is not the suggested version may cause unexpected issues.
|
||||
|
||||
Do you want to proceed anyways?"</string>
|
||||
Do you want to proceed anyways?</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 and customization of options</string>
|
||||
<string name="patch_selection_safeguard_confirmation">"Changing the selection of patches may cause unexpected issues.
|
||||
<string name="patch_selection_safeguard_confirmation">Changing the selection of patches may cause unexpected issues.
|
||||
|
||||
Enable anyways?"</string>
|
||||
Enable anyways?</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_confirmation">"Universal patches are not as well tested as those that target specific apps.
|
||||
<string name="universal_patches_safeguard_confirmation">Universal patches are not as well tested as those that target specific apps.
|
||||
|
||||
Enable anyways?"</string>
|
||||
Enable anyways?</string>
|
||||
<string name="import_keystore">Import keystore</string>
|
||||
<string name="import_keystore_description">Import a custom keystore</string>
|
||||
<string name="import_keystore_dialog_title">Enter keystore credentials</string>
|
||||
@@ -143,9 +146,9 @@ Enable anyways?"</string>
|
||||
<string name="export_keystore_success">Exported keystore</string>
|
||||
<string name="regenerate_keystore">Regenerate keystore</string>
|
||||
<string name="regenerate_keystore_description">Generate a new keystore</string>
|
||||
<string name="regenerate_keystore_dialog_description">"You are about to regenerate your keystore the manager will use during the patching process.
|
||||
<string name="regenerate_keystore_dialog_description">You are about to regenerate your keystore the manager will use during the patching process.
|
||||
|
||||
You will not be able to update the previously installed apps from this source."</string>
|
||||
You will not be able to update the previously installed apps from this source.</string>
|
||||
<string name="regenerate_keystore_success">The keystore has been successfully replaced</string>
|
||||
<string name="import_patch_selection">Import patch selection</string>
|
||||
<string name="import_patch_selection_description">Import patch selection from a JSON file</string>
|
||||
@@ -270,23 +273,23 @@ You will not be able to update the previously installed apps from this source."<
|
||||
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
|
||||
<string name="patch_options_reset_toast">Patch options have been reset</string>
|
||||
<string name="non_suggested_version_warning_title">Non suggested version</string>
|
||||
<string name="non_suggested_version_warning_description">"The version of the app you have selected does not match the suggested version.
|
||||
<string name="non_suggested_version_warning_description">The version of the app you have selected does not match the suggested version.
|
||||
Please use the suggested version: %s
|
||||
|
||||
To continue anyway, disable \"Require suggested app version\" in the advanced settings."</string>
|
||||
To continue anyway, disable \"Require suggested app version\" in the advanced settings.</string>
|
||||
<string name="selection_warning_title">Stop using defaults?</string>
|
||||
<string name="selection_warning_description">"It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.
|
||||
<string name="selection_warning_description">It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.
|
||||
|
||||
You need to turn on \"Allow changing patch selection and options\" in the advanced settings before toggling patches."</string>
|
||||
<string name="universal_patch_warning_description">"Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.
|
||||
You need to turn on \"Allow changing patch selection and options\" in the advanced settings before toggling patches.</string>
|
||||
<string name="universal_patch_warning_description">Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.
|
||||
|
||||
You need to turn on \"Allow using universal patches\" in the advanced settings before using universal patches."</string>
|
||||
You need to turn on \"Allow using universal patches\" in the advanced settings before using universal patches.</string>
|
||||
<string name="this_version">This version</string>
|
||||
<string name="universal">Any app</string>
|
||||
<string name="search_patches">Search patches</string>
|
||||
<string name="app_version_not_compatible">"This patch is not compatible with the selected app version (%1$s)
|
||||
<string name="app_version_not_compatible">This patch is not compatible with the selected app version (%1$s)
|
||||
|
||||
It is only compatible with the following version(s): %2$s"</string>
|
||||
It is only compatible with the following version(s): %2$s</string>
|
||||
<string name="continue_with_version">Continue with this version?</string>
|
||||
<string name="version_not_compatible">Not all patches are compatible with this version (%s). Do you want to continue anyway?</string>
|
||||
<string name="download_application">Download application?</string>
|
||||
@@ -380,6 +383,7 @@ It is only compatible with the following version(s): %2$s"</string>
|
||||
<string name="less">Less</string>
|
||||
<string name="continue_">Continue</string>
|
||||
<string name="dismiss">Dismiss</string>
|
||||
<string name="view_announcement">View announcement</string>
|
||||
<string name="permanent_dismiss">Do not show this again</string>
|
||||
<string name="donate">Donate</string>
|
||||
<string name="website">Website</string>
|
||||
@@ -398,6 +402,8 @@ It is only compatible with the following version(s): %2$s"</string>
|
||||
<string name="patches_delete_single_dialog_description">Are you sure you want to delete \"%s\"?</string>
|
||||
<string name="patches_delete_multiple_dialog_description">Are you sure you want to delete the selected patches?</string>
|
||||
|
||||
<string name="announcements">Announcements</string>
|
||||
<string name="archive">Archive</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="developer_options_taps">%d taps remaining</string>
|
||||
@@ -428,8 +434,8 @@ It is only compatible with the following version(s): %2$s"</string>
|
||||
<string name="save_with_count">Save (%1$s)</string>
|
||||
<string name="update">Update</string>
|
||||
<string name="empty">Empty</string>
|
||||
<string name="installing_message">"Tap on <b>Update</b> when prompted.
|
||||
ReVanced Manager will close when updating."</string>
|
||||
<string name="installing_message">Tap on <b>Update</b> when prompted.
|
||||
ReVanced Manager will close when updating.</string>
|
||||
<string name="no_changelogs_found">No changelogs found</string>
|
||||
<string name="just_now">Just now</string>
|
||||
<string name="minutes_ago">%sm ago</string>
|
||||
@@ -443,14 +449,15 @@ ReVanced Manager will close when updating."</string>
|
||||
|
||||
<string name="failed_to_check_updates">Failed to check for updates: %s</string>
|
||||
<string name="no_update_available">No update available</string>
|
||||
<string name="no_announcements_found">No announcements found</string>
|
||||
<string name="update_check">Checking for updates…</string>
|
||||
<string name="dismiss_temporary">Not now</string>
|
||||
<string name="update_available_dialog_description">A new version of ReVanced Manager (%s) is available.</string>
|
||||
<string name="failed_to_download_update">Failed to download update: %s</string>
|
||||
<string name="download">Download</string>
|
||||
<string name="download_confirmation_metered">"You are currently on a metered connection, and data charges from your service provider may apply.
|
||||
<string name="download_confirmation_metered">You are currently on a metered connection, and data charges from your service provider may apply.
|
||||
|
||||
Do you still want to continue?"</string>
|
||||
Do you still want to continue?</string>
|
||||
<string name="download_update_confirmation">Download update?</string>
|
||||
<string name="no_contributors_found">No contributors found</string>
|
||||
<string name="select">Select</string>
|
||||
@@ -488,9 +495,9 @@ Do you still want to continue?"</string>
|
||||
<string name="patches_prereleases">Use pre-releases</string>
|
||||
<string name="patches_prereleases_description">Use pre-release versions of %s</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).
|
||||
<string name="incompatible_patches_dialog">These patches are not compatible with the selected app version (%1$s).
|
||||
|
||||
Click on the patches to see more details."</string>
|
||||
Click on the patches to see more details.</string>
|
||||
<string name="incompatible_patch">Incompatible patch</string>
|
||||
<string name="any_version">Any</string>
|
||||
<string name="never_show_again">Never show again</string>
|
||||
@@ -499,4 +506,5 @@ Click on the patches to see more details."</string>
|
||||
<string name="failed_to_import_keystore">Failed to import keystore</string>
|
||||
<string name="export">Export</string>
|
||||
<string name="confirm">Confirm</string>
|
||||
<string name="new_announcement">New announcement:\n%s</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
ktx = "1.17.0"
|
||||
material3 = "1.4.0"
|
||||
material3 = "1.5.0-alpha14"
|
||||
ui-tooling = "1.10.0"
|
||||
viewmodel-lifecycle = "2.10.0"
|
||||
splash-screen = "1.2.0"
|
||||
@@ -9,7 +9,7 @@ appcompat = "1.7.1"
|
||||
preferences-datastore = "1.2.0"
|
||||
work-runtime = "2.11.0"
|
||||
compose-bom = "2025.12.01"
|
||||
navigation = "2.9.6"
|
||||
navigation = "2.9.7"
|
||||
accompanist = "0.37.3"
|
||||
placeholder = "1.0.12"
|
||||
reorderable = "3.0.0"
|
||||
@@ -17,13 +17,13 @@ serialization = "1.9.0"
|
||||
collection = "0.4.0"
|
||||
datetime = "0.7.1"
|
||||
room-version = "2.8.4"
|
||||
revanced-patcher = "21.0.0"
|
||||
revanced-library = "3.0.2"
|
||||
revanced-patcher = "22.0.0"
|
||||
revanced-library = "4.0.0"
|
||||
koin = "4.1.1"
|
||||
ktor = "3.3.3"
|
||||
markdown-renderer = "0.39.0"
|
||||
fading-edges = "1.0.4"
|
||||
kotlin = "2.3.0"
|
||||
kotlin = "2.3.10"
|
||||
android-gradle-plugin = "8.13.2"
|
||||
dev-tools-gradle-plugin = "2.3.4"
|
||||
about-libraries = "13.2.1"
|
||||
@@ -81,8 +81,8 @@ room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room-ver
|
||||
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room-version" }
|
||||
|
||||
# Patcher
|
||||
revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "revanced-patcher" }
|
||||
revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" }
|
||||
revanced-patcher = { group = "app.revanced", name = "patcher-android", version.ref = "revanced-patcher" }
|
||||
revanced-library = { group = "app.revanced", name = "library-android", version.ref = "revanced-library" }
|
||||
|
||||
# Koin
|
||||
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
google()
|
||||
gradlePluginPortal()
|
||||
@@ -8,6 +9,7 @@ pluginManagement {
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
google()
|
||||
maven("https://jitpack.io")
|
||||
|
||||
Reference in New Issue
Block a user